diff --git a/README.md b/README.md index a607ec8..518e56a 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,12 @@ and MUST NOT be used as id prefix for stored `Record`._ Having container stored with `foo` identifier would make `foo.bar` record inaccessible. The rules might be hard to follow with multiple entries and sub-containers, so runtime checks were implemented. To instantiate `Setup` with integrity checks instantiate with static -constructor: `$setup = Setup::secure();` - more on that in following sections. +constructor: `$setup = Setup::validated();` - more on that in following sections. #### Preconfigured setup - constructor parameters -`Setup` may be instantiated with [`Setup\Collection`](src/Setup/Collection.php) parameter that -may contain already configured records or sub-containers. This collection can be also created -with associative arrays passed to static constructor `Setup::withData()`. Both `Collection` and +`Setup` may be instantiated with implementation of [`Builder`](src/Builder.php) parameter that +may contain already configured records or sub-containers. This builder can be also created +with associative arrays passed to static constructor `Setup::withData()`. Both `Builder` and `Setup::withData()` method takes associative `Recrod[]` and `ContainerInetrface[]` arrays. ```php @@ -148,7 +148,7 @@ $containers = [ // ... ]; -$setup = new Setup(new Setup\Collection($records, $containers)); +$setup = new Setup(new Builder\ProtectedBuilder($records, $containers)); // or $setup = Setup::withData($records, $containers); ``` @@ -159,22 +159,30 @@ work with invalid configuration anyway. In development environment those checks because they allow to fail as early as possible and help localize the source of an error. #### Secure setup & circular reference detection -Instantiating `Setup` with [`Setup\ValidatedCollection`](src/Setup/ValidatedCollection.php), -with `Setup::secure()` static constructor or passing `true` as third parameter of `Setup::withData()` +By default container setup doesn't check if given identifiers are already defined or whether +will cause name collision making some entries inaccessible (sub-containers with identifier used +record entry prefix). + +Instantiating `Setup` with [`Builder\ValidatedBuilder`](src/Builder/ValidatedBuilder.php) +class, `Setup::validated()` static constructor or passing `true` as third parameter of `Setup::withData()` will enable runtime integrity checks for container configuration and detect circular references when resolving dependencies with recursive container calls. Container will be created with identifiers that will be accessible, **call stack** will be added to all exceptions thrown from container, and `ContainerInterface::get()` method will throw `CircularReferenceException` immediately after subsequent call would try to retrieve currently resolved record without blowing up the stack. -> This feature comes with minor performance overhead, and checking this kind of developer errors have -almost no value in production. It is recommended to use it as **development tool** only. +If you want to use container setup overriding some default values, you need to disable overwrite checks +in validation mode. **Overwrite** might be allowed despite validation by instantiating `ValidatedBuilder` +with `$allowOverwrite` (third) parameter. Instantiating with `true`, passing it to `Setup::validated();` +or as forth parameter of `Setup::withData()` will have same effect. + +> These checks are separated from default setup behavior, because they should not be required in production +environment. It is recommended to use them as **development tool** though. -Checking such errors just because you can is pointless, because there are much more config related -bugs that cannot be checked in other way than testing if application works before deploying it to -production (invalid values or identifiers in container calling methods). On the other hand if those -checks are causing visible drop in performance you probably using container to extensively (see -[recommended use](#recommended-use) section). +Integration tests are necessary in development, because misconfigured container will most likely crash +the application, and it cannot be controlled by code anyway. This way some performance overhead might be +eliminated from production, but if those checks are causing visible drop in performance you are probably +using container too extensively (see [recommended use](#recommended-use) section). #### Composed entry - nested composition & decorator feature: Entry called for existing record can reassign it with `compose()` method if it uses it @@ -197,14 +205,15 @@ currently using it would fail on type-checking, and due to lazy instantiation co can't ensure decorator use and possible errors will emerge at runtime. #### Direct instantiation & container composition -All `Setup` does, beside ability to validate configuration, is providing help to compose final container -based on provided configuration and chosen options. The simple container with `Record[]` array would be -instantiated like this: +All `Setup` does, beside ability to validate configuration, is providing helper methods creating +Record entries and creating various container compositions based on given setup. Container can also +0be instantiated directly - for example simple container (with `Record[]` array) would be instantiated +this way: ```php $container = new RecordContainer(new Records($records)); ``` -And here's an example composition of container with circular reference checking and encapsulated -sub-containers (`ContainerInterface[]` array): +When container needs circular reference checking and encapsulate some sub-containers it would need +to be instantiated (with `ContainerInterface[]` array) this way: ```php $container = new CompositeContainer(new TrackedRecords($records), $containers); ``` diff --git a/src/Setup/Collection.php b/src/Builder.php similarity index 68% rename from src/Setup/Collection.php rename to src/Builder.php index 2e398a3..908780d 100644 --- a/src/Setup/Collection.php +++ b/src/Builder.php @@ -9,18 +9,13 @@ * with this source code in the file LICENSE. */ -namespace Polymorphine\Container\Setup; +namespace Polymorphine\Container; -use Polymorphine\Container\Records; -use Polymorphine\Container\RecordContainer; -use Polymorphine\Container\CompositeContainer; -use Polymorphine\Container\Exception; use Psr\Container\ContainerInterface; -class Collection +class Builder { - protected const SEPARATOR = CompositeContainer::SEPARATOR; protected const WRAP_PREFIX = 'WRAP>'; protected $records; @@ -39,25 +34,17 @@ public function __construct(array $records = [], array $containers = []) public function container(): ContainerInterface { return $this->containers - ? new CompositeContainer(new Records($this->records), $this->containers) - : new RecordContainer(new Records($this->records)); + ? new CompositeContainer($this->records(), $this->containers) + : new RecordContainer($this->records()); } public function addRecord(string $id, Records\Record $record): void { - if (isset($this->records[$id])) { - throw Exception\InvalidIdException::alreadyDefined("`$id` record"); - } - $this->records[$id] = $record; } public function addContainer(string $id, ContainerInterface $container): void { - if (isset($this->containers[$id])) { - throw Exception\InvalidIdException::alreadyDefined("`$id` container"); - } - $this->containers[$id] = $container; } @@ -74,6 +61,11 @@ public function wrapRecord(string $id): string return $newId; } + protected function records(): Records + { + return new Records($this->records); + } + private function wrappedId(string $id): string { $newId = static::WRAP_PREFIX . $id; diff --git a/src/Setup/Entry.php b/src/Builder/Entry.php similarity index 90% rename from src/Setup/Entry.php rename to src/Builder/Entry.php index 692a37b..b1387dc 100644 --- a/src/Setup/Entry.php +++ b/src/Builder/Entry.php @@ -9,8 +9,9 @@ * with this source code in the file LICENSE. */ -namespace Polymorphine\Container\Setup; +namespace Polymorphine\Container\Builder; +use Polymorphine\Container\Builder; use Polymorphine\Container\Records\Record; use Polymorphine\Container\Exception; use Psr\Container\ContainerInterface; @@ -23,12 +24,12 @@ class Entry { private $name; - private $records; + private $builder; - public function __construct(string $name, Collection $records) + public function __construct(string $name, Builder $builder) { $this->name = $name; - $this->records = $records; + $this->builder = $builder; } /** @@ -41,7 +42,7 @@ public function __construct(string $name, Collection $records) */ public function record(Record $record): void { - $this->records->addRecord($this->name, $record); + $this->builder->addRecord($this->name, $record); } /** @@ -95,7 +96,7 @@ public function compose(string $className, string ...$dependencies): void { $idx = array_search($this->name, $dependencies, true); if ($idx !== false) { - $dependencies[$idx] = $this->records->wrapRecord($this->name); + $dependencies[$idx] = $this->builder->wrapRecord($this->name); } $this->record(new Record\ComposeRecord($className, ...$dependencies)); @@ -130,6 +131,6 @@ public function create(string $factoryId, string $method, string ...$arguments): */ public function container(ContainerInterface $container) { - $this->records->addContainer($this->name, $container); + $this->builder->addContainer($this->name, $container); } } diff --git a/src/Setup/ValidatedCollection.php b/src/Builder/ValidatedBuilder.php similarity index 74% rename from src/Setup/ValidatedCollection.php rename to src/Builder/ValidatedBuilder.php index dd0158d..35b6a93 100644 --- a/src/Setup/ValidatedCollection.php +++ b/src/Builder/ValidatedBuilder.php @@ -9,44 +9,50 @@ * with this source code in the file LICENSE. */ -namespace Polymorphine\Container\Setup; +namespace Polymorphine\Container\Builder; +use Polymorphine\Container\Builder; use Polymorphine\Container\Records; -use Polymorphine\Container\RecordContainer; -use Polymorphine\Container\CompositeContainer; use Polymorphine\Container\Exception; +use Polymorphine\Container\CompositeContainer; use Psr\Container\ContainerInterface; -class ValidatedCollection extends Collection +class ValidatedBuilder extends Builder { + private $allowOverwrite; private $reservedIds = []; - public function __construct(array $records = [], array $containers = []) + public function __construct(array $records = [], array $containers = [], bool $allowOverwrite = false) { parent::__construct($records, $containers); + $this->allowOverwrite = $allowOverwrite; $this->validateState(); } - public function container(): ContainerInterface - { - return $this->containers - ? new CompositeContainer(new Records\TrackedRecords($this->records), $this->containers) - : new RecordContainer(new Records\TrackedRecords($this->records)); - } - public function addRecord(string $id, Records\Record $record): void { $this->checkRecordId($id); + if (!$this->allowOverwrite && isset($this->records[$id])) { + throw Exception\InvalidIdException::alreadyDefined("`$id` record"); + } parent::addRecord($id, $record); } public function addContainer(string $id, ContainerInterface $container): void { $this->checkContainerId($id); + if (!$this->allowOverwrite && isset($this->containers[$id])) { + throw Exception\InvalidIdException::alreadyDefined("`$id` container"); + } parent::addContainer($id, $container); } + protected function records(): Records + { + return new Records\TrackedRecords($this->records); + } + private function validateState() { array_map([$this, 'checkRecord'], array_keys($this->records), $this->records); @@ -67,7 +73,7 @@ private function checkRecordId(string $id): void throw Exception\InvalidIdException::alreadyDefined("`$id` container"); } - $separator = strpos($id, self::SEPARATOR); + $separator = strpos($id, CompositeContainer::SEPARATOR); $reserved = $separator === false ? $id : substr($id, 0, $separator); if (isset($this->containers[$reserved])) { throw Exception\InvalidIdException::prefixConflict($reserved); @@ -86,8 +92,8 @@ private function checkContainer(string $id, $value): void private function checkContainerId(string $id): void { - if (strpos($id, self::SEPARATOR) !== false) { - throw Exception\InvalidIdException::unexpectedPrefixSeparator(self::SEPARATOR, $id); + if (strpos($id, CompositeContainer::SEPARATOR) !== false) { + throw Exception\InvalidIdException::unexpectedPrefixSeparator(CompositeContainer::SEPARATOR, $id); } if (isset($this->reservedIds[$id])) { diff --git a/src/Setup.php b/src/Setup.php index 228e63c..c727f43 100644 --- a/src/Setup.php +++ b/src/Setup.php @@ -16,11 +16,11 @@ class Setup { - private $collection; + private $builder; - public function __construct(Setup\Collection $collection = null) + public function __construct(Builder $builder = null) { - $this->collection = $collection ?: new Setup\Collection(); + $this->builder = $builder ?: new Builder(); } /** @@ -29,32 +29,45 @@ public function __construct(Setup\Collection $collection = null) * Added entries will be validated for identifier conflicts and * created container will be monitored for circular references. * + * Additional $allowOverwrite parameter determines if adding entry with + * already defined id will be overwritten. Can be used to build container + * with container with default values that can change under some conditions. + * + * @param bool $allowOverwrite + * * @return self */ - public static function secure(): self + public static function validated(bool $allowOverwrite = false): self { - return new self(new Setup\ValidatedCollection()); + return new self(new Builder\ValidatedBuilder([], [], $allowOverwrite)); } /** * Creates Setup with predefined configuration. * - * If `true` is passed as $validate param secure version of Setup - * will be created and predefined configuration will be validated. + * If `true` is passed as $validate param validated version of Setup + * will be created. Both passed data and added entries will be validated. * - * @see Setup::secure() + * Additional $allowOverwrite parameter determines if adding entry with + * already defined id will be overwritten. Can be used to build container + * with container with default values that can change under some conditions. * * @param Records\Record[] $records * @param ContainerInterface[] $containers * @param bool $validate + * @param bool $allowOverwrite * * @return self */ - public static function withData(array $records = [], array $containers = [], bool $validate = false): self - { + public static function withData( + array $records = [], + array $containers = [], + bool $validate = false, + bool $allowOverwrite = false + ): self { $collection = $validate - ? new Setup\ValidatedCollection($records, $containers) - : new Setup\Collection($records, $containers); + ? new Builder\ValidatedBuilder($records, $containers, $allowOverwrite) + : new Builder($records, $containers); return new self($collection); } @@ -69,7 +82,7 @@ public static function withData(array $records = [], array $containers = [], boo */ public function container(): ContainerInterface { - return $this->collection->container(); + return $this->builder->container(); } /** @@ -78,11 +91,11 @@ public function container(): ContainerInterface * * @param string $name * - * @return Setup\Entry + * @return Builder\Entry */ - public function entry(string $name): Setup\Entry + public function entry(string $name): Builder\Entry { - return new Setup\Entry($name, $this->collection); + return new Builder\Entry($name, $this->builder); } /** @@ -95,7 +108,7 @@ public function entry(string $name): Setup\Entry public function records(array $records): void { foreach ($records as $id => $record) { - $this->collection->addRecord($id, $record); + $this->builder->addRecord($id, $record); } } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 3e6c7ec..8866471 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Polymorphine\Container\ConfigContainer; use Polymorphine\Container\Setup; +use Polymorphine\Container\Builder; use Polymorphine\Container\Records\Record; use Polymorphine\Container\Exception; use Polymorphine\Container\Tests\Fixtures\Example; @@ -145,17 +146,25 @@ public function testCallbacksCannotModifyRegistry() $this->assertFalse($setup->container()->get('lazyModifier')); } - public function testOverwritingExistingKey_ThrowsException() + public function testOverwritingExistingKey_ValidatedWithOverwriteLock_ThrowsException() { - $setup = Setup::withData(); + $setup = Setup::withData([], [], true, false); $setup->entry('test')->value('foo'); $this->expectException(Exception\InvalidIdException::class); $setup->entry('test')->value('bar'); } + public function testOverwritingExistingKey_ValidatedWithoutOverwriteLock_OverwritesRecordValue() + { + $setup = Setup::withData([], [], true, true); + $setup->entry('test')->value('foo'); + $setup->entry('test')->value('bar'); + $this->assertSame('bar', $setup->container()->get('test')); + } + public function testAddingRecordsArrayWithExistingRecord_ThrowsException() { - $setup = Setup::withData(['exists' => new Record\ValueRecord('something')]); + $setup = Setup::withData(['exists' => new Record\ValueRecord('something')], [], true); $this->expectException(Exception\InvalidIdException::class); $setup->records(['notExists' => new Record\ValueRecord('foo'), 'exists' => new Record\ValueRecord('bar')]); } @@ -198,14 +207,22 @@ public function testBuildConfigContainerWithSetup() $this->assertSame('value', $container->get('cfg.test')); } - public function testOverwritingContainerId_ThrowsException() + public function testOverwritingContainerId_ValidatedWithOverwriteLock_ThrowsException() { - $setup = Setup::withData(); + $setup = Setup::withData([], [], true); $setup->entry('data')->container(new ConfigContainer([])); $this->expectException(Exception\InvalidIdException::class); $setup->entry('data')->container(new ConfigContainer([])); } + public function testOverwritingContainerId_ValidatedWithOverwriteLock__OverwritesContainerValue() + { + $setup = Setup::withData([], [], true, true); + $setup->entry('data')->container(new ConfigContainer([])); + $setup->entry('data')->container($changedContainer = new ConfigContainer([])); + $this->assertSame($changedContainer, $setup->container()->get('data')); + } + public function testSetupContainer_ReturnsNewInstanceOfContainer() { $config = ['env' => new ConfigContainer(['config' => 'value'])]; @@ -332,8 +349,8 @@ public function undefinedPaths(): array public function testInstantiatingSecureSetup() { - $this->assertEquals(Setup::secure(), new Setup(new Setup\ValidatedCollection())); - $this->assertEquals(Setup::secure(), Setup::withData([], [], true)); + $this->assertEquals(Setup::validated(), new Setup(new Builder\ValidatedBuilder())); + $this->assertEquals(Setup::validated(), Setup::withData([], [], true)); } public function testContainerIdWithIdSeparator_SecureSetupThrowsException()