diff --git a/README.md b/README.md index 11bd540..97cbf8f 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ probably using container too extensively (see [recommended use](#recommended-use #### Composed entries ##### Record composition using Wrapper Entry may be built with multiple instance descriptors (same parameters as `InstanceRecord` uses) -given in chained [`Wrapper`](src/Setup/Entry/Wrapper.php) calls: +given in chained [`Wrapper`](src/Setup/Wrapper.php) calls: ```php $setup->add('A') ->wrappedInstance(SomeClass::class, 'B', 'C') diff --git a/src/Setup.php b/src/Setup.php index 8175158..166a03a 100644 --- a/src/Setup.php +++ b/src/Setup.php @@ -11,23 +11,19 @@ namespace Polymorphine\Container; +use Polymorphine\Container\Setup\Build; use Polymorphine\Container\Setup\Entry; +use Polymorphine\Container\Setup\Exception; use Psr\Container\ContainerInterface; -abstract class Setup +class Setup { - protected $records; - protected $containers; + private $build; - /** - * @param Records\Record[] $records - * @param ContainerInterface[] $containers - */ - public function __construct(array $records = [], array $containers = []) + public function __construct(Build $build = null) { - $this->records = $records; - $this->containers = $containers; + $this->build = $build ?: new Setup\Build(); } /** @@ -36,20 +32,24 @@ public function __construct(array $records = [], array $containers = []) * * @return static */ - public static function basic(array $records = [], array $containers = []): self + public static function production(array $records = [], array $containers = []): self { - return new Setup\BasicSetup($records, $containers); + return new self(new Setup\Build($records, $containers)); } /** + * Creates Setup with additional identifier collision checks, and + * Container created with such Setup will also detect circular + * references and add call stack paths to thrown exceptions. + * * @param Records\Record[] $records * @param ContainerInterface[] $containers * * @return static */ - public static function validated(array $records = [], array $containers = []): self + public static function development(array $records = [], array $containers = []): self { - return new Setup\ValidatedSetup($records, $containers); + return new self(new Setup\Build\ValidatedBuild($records, $containers)); } /** @@ -63,35 +63,60 @@ public static function validated(array $records = [], array $containers = []): s */ public function container(): ContainerInterface { - return $this->containers - ? new CompositeContainer($this->records(), $this->containers) - : new RecordContainer($this->records()); + return $this->build->container(); } /** - * Returns Entry object able to add new data to container configuration - * for given identifier. + * Returns Entry object adding new container configuration data + * for given identifier. For already defined identifiers Exception + * will be thrown. * * @param string $id * + * @throws Exception\OverwriteRuleException + * * @return Setup\Entry */ - public function add(string $id): Entry + public function set(string $id): Entry { - return new Entry\AddEntry($id, $this); + if ($this->build->has($id)) { + throw Exception\OverwriteRuleException::alreadyDefined($id); + } + return new Entry($id, $this->build); } /** - * Returns Entry object able to replace data in container configuration - * for given identifier. + * Returns Entry object replacing container configuration data + * for given identifier. For undefined identifiers Exception will + * be thrown. * * @param string $id * + * @throws Exception\OverwriteRuleException + * * @return Setup\Entry */ public function replace(string $id): Entry { - return new Entry\ReplaceEntry($id, $this); + if (!$this->build->has($id)) { + throw Exception\OverwriteRuleException::undefined($id); + } + return new Entry($id, $this->build); + } + + /** + * Returns Entry object adding new container configuration data + * for given identifier. For already defined identifiers returned + * entry will not change configuration. + * + * @param string $id + * + * @return Setup\Entry + */ + public function fallback(string $id): Entry + { + $build = $this->build->has($id) ? new Build() : $this->build; + return new Entry($id, $build); } /** @@ -109,64 +134,12 @@ public function replace(string $id): Entry * * @param string $id * - * @throws Setup\Exception\IntegrityConstraintException + * @throws Exception\OverwriteRuleException * - * @return Setup\Entry\Wrapper + * @return Setup\Wrapper */ - public function decorate(string $id): Entry\Wrapper + public function decorate(string $id): Setup\Wrapper { - if (!isset($this->records[$id])) { - throw Setup\Exception\IntegrityConstraintException::undefined($id); - } - - return new Entry\Wrapper($id, $this->records[$id], new Entry\ReplaceEntry($id, $this)); + return $this->build->decorator($id); } - - /** - * Adds Record instances directly to container configuration. - * - * @param Records\Record[] $records Flat associative array of Record instances - * - * @throws Setup\Exception\IntegrityConstraintException - */ - public function addRecords(array $records): void - { - foreach ($records as $id => $record) { - $this->addRecord($id, $record); - } - } - - /** - * @param string $id - * @param Records\Record $record - * - * @throws Setup\Exception\IntegrityConstraintException - */ - abstract public function addRecord(string $id, Records\Record $record): void; - - /** - * @param string $id - * @param ContainerInterface $container - * - * @throws Setup\Exception\IntegrityConstraintException - */ - abstract public function addContainer(string $id, ContainerInterface $container): void; - - /** - * @param string $id - * @param Records\Record $record - * - * @throws Setup\Exception\IntegrityConstraintException - */ - abstract public function replaceRecord(string $id, Records\Record $record): void; - - /** - * @param string $id - * @param ContainerInterface $container - * - * @throws Setup\Exception\IntegrityConstraintException - */ - abstract public function replaceContainer(string $id, ContainerInterface $container): void; - - abstract protected function records(): Records; } diff --git a/src/Setup/BasicSetup.php b/src/Setup/BasicSetup.php deleted file mode 100644 index 27c3a69..0000000 --- a/src/Setup/BasicSetup.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Setup; - -use Polymorphine\Container\Setup; -use Polymorphine\Container\Records; -use Psr\Container\ContainerInterface; - - -class BasicSetup extends Setup -{ - public function addRecord(string $id, Records\Record $record): void - { - $this->records[$id] = $record; - } - - public function addContainer(string $id, ContainerInterface $container): void - { - $this->containers[$id] = $container; - } - - public function replaceRecord(string $id, Records\Record $record): void - { - $this->records[$id] = $record; - } - - public function replaceContainer(string $id, ContainerInterface $container): void - { - $this->containers[$id] = $container; - } - - protected function records(): Records - { - return new Records($this->records); - } -} diff --git a/src/Setup/Build.php b/src/Setup/Build.php new file mode 100644 index 0000000..6cb8744 --- /dev/null +++ b/src/Setup/Build.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Setup; + +use Polymorphine\Container\RecordContainer; +use Polymorphine\Container\CompositeContainer; +use Polymorphine\Container\Records; +use Psr\Container\ContainerInterface; + + +class Build implements Collection +{ + protected $records; + protected $containers; + + public function __construct(array $records = [], array $containers = []) + { + $this->records = $records; + $this->containers = $containers; + } + + public function container(): ContainerInterface + { + return $this->containers + ? new CompositeContainer($this->records(), $this->containers) + : new RecordContainer($this->records()); + } + + public function has(string $id): bool + { + return isset($this->records[$id]) || isset($this->containers[$id]); + } + + public function setRecord(string $id, Records\Record $record): void + { + $this->records[$id] = $record; + } + + public function setContainer(string $id, ContainerInterface $container): void + { + $this->containers[$id] = $container; + } + + public function decorator(string $id): Wrapper + { + if (!isset($this->records[$id])) { + throw Exception\OverwriteRuleException::undefined($id); + } + + return new Wrapper($id, $this->records[$id], new Entry($id, $this)); + } + + protected function records(): Records + { + return new Records($this->records); + } +} diff --git a/src/Setup/Build/ValidatedBuild.php b/src/Setup/Build/ValidatedBuild.php new file mode 100644 index 0000000..a056e2b --- /dev/null +++ b/src/Setup/Build/ValidatedBuild.php @@ -0,0 +1,92 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Setup\Build; + +use Polymorphine\Container\Setup\Build; +use Polymorphine\Container\Setup\Exception; +use Polymorphine\Container\Records; +use Polymorphine\Container\CompositeContainer; +use Psr\Container\ContainerInterface; + + +class ValidatedBuild extends Build +{ + private $reservedIds = []; + + public function __construct(array $records = [], array $containers = []) + { + parent::__construct($this->validRecords($records), $this->validContainers($containers)); + } + + public function setRecord(string $id, Records\Record $record): void + { + $this->checkRecordId($id); + parent::setRecord($id, $record); + } + + public function setContainer(string $id, ContainerInterface $container): void + { + $this->checkContainerId($id); + parent::setContainer($id, $container); + } + + protected function records(): Records + { + return new Records\TrackedRecords($this->records); + } + + private function checkRecordId(string $id): void + { + $separator = strpos($id, CompositeContainer::SEPARATOR); + if (!$separator) { return; } + + $reserved = substr($id, 0, $separator); + if (isset($this->containers[$reserved])) { + throw Exception\IntegrityConstraintException::prefixConflict($reserved); + } + + $this->reservedIds[$reserved] = true; + } + + private function checkContainerId(string $id): void + { + if (strpos($id, CompositeContainer::SEPARATOR) !== false) { + throw Exception\IntegrityConstraintException::unexpectedPrefixSeparator(CompositeContainer::SEPARATOR, $id); + } + + if (isset($this->reservedIds[$id])) { + throw Exception\IntegrityConstraintException::alreadyDefined($id); + } + } + + private function validRecords(array $records): array + { + foreach ($records as $id => $record) { + $this->checkRecordId($id); + if (!$record instanceof Records\Record) { + throw Exception\InvalidTypeException::recordExpected($id); + } + } + return $records; + } + + private function validContainers(array $containers): array + { + foreach ($containers as $id => $container) { + $this->checkContainerId($id); + if (!$container instanceof ContainerInterface) { + throw Exception\InvalidTypeException::containerExpected($id); + } + } + return $containers; + } +} diff --git a/src/Setup/Collection.php b/src/Setup/Collection.php new file mode 100644 index 0000000..9e9b550 --- /dev/null +++ b/src/Setup/Collection.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Setup; + +use Polymorphine\Container\Records; +use Psr\Container\ContainerInterface; + + +interface Collection +{ + public function has(string $id): bool; + + public function setRecord(string $id, Records\Record $record): void; + + public function setContainer(string $id, ContainerInterface $container): void; +} diff --git a/src/Setup/Entry.php b/src/Setup/Entry.php index 5eff5f1..872937c 100644 --- a/src/Setup/Entry.php +++ b/src/Setup/Entry.php @@ -11,7 +11,6 @@ namespace Polymorphine\Container\Setup; -use Polymorphine\Container\Setup; use Polymorphine\Container\Records\Record; use Psr\Container\ContainerInterface; @@ -20,15 +19,15 @@ * Write-only proxy with helper methods to instantiate and set * Record implementations for given Container item identifier. */ -abstract class Entry +class Entry { protected $id; protected $builder; - public function __construct(string $id, Setup $builder) + public function __construct(string $id, Collection $build) { $this->id = $id; - $this->builder = $builder; + $this->builder = $build; } /** @@ -39,7 +38,10 @@ public function __construct(string $id, Setup $builder) * * @throws Exception\IntegrityConstraintException */ - abstract public function record(Record $record): void; + public function record(Record $record): void + { + $this->builder->setRecord($this->id, $record); + } /** * Sets ContainerInterface instance as sub-container that may @@ -52,7 +54,10 @@ abstract public function record(Record $record): void; * * @throws Exception\IntegrityConstraintException */ - abstract public function container(ContainerInterface $container): void; + public function container(ContainerInterface $container): void + { + $this->builder->setContainer($this->id, $container); + } /** * Adds ValueRecord with given value into container records. @@ -112,16 +117,16 @@ public function instance(string $className, string ...$dependencies): void * add ComposedInstanceRecord to container records. * * @see Record\InstanceRecord - * @see Entry\Wrapper + * @see Wrapper * * @param string $className * @param string ...$dependencies * - * @return Entry\Wrapper + * @return Wrapper */ - public function wrappedInstance(string $className, string ...$dependencies): Entry\Wrapper + public function wrappedInstance(string $className, string ...$dependencies): Wrapper { - return new Entry\Wrapper($this->id, new Record\InstanceRecord($className, ...$dependencies), $this); + return new Wrapper($this->id, new Record\InstanceRecord($className, ...$dependencies), $this); } /** diff --git a/src/Setup/Entry/AddEntry.php b/src/Setup/Entry/AddEntry.php deleted file mode 100644 index 8c4aa47..0000000 --- a/src/Setup/Entry/AddEntry.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Setup\Entry; - -use Polymorphine\Container\Records\Record; -use Polymorphine\Container\Setup\Entry; -use Psr\Container\ContainerInterface; - - -class AddEntry extends Entry -{ - public function record(Record $record): void - { - $this->builder->addRecord($this->id, $record); - } - - public function container(ContainerInterface $container): void - { - $this->builder->addContainer($this->id, $container); - } -} diff --git a/src/Setup/Entry/ReplaceEntry.php b/src/Setup/Entry/ReplaceEntry.php deleted file mode 100644 index 8ac6cc8..0000000 --- a/src/Setup/Entry/ReplaceEntry.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Setup\Entry; - -use Polymorphine\Container\Records\Record; -use Polymorphine\Container\Setup\Entry; -use Psr\Container\ContainerInterface; - - -class ReplaceEntry extends Entry -{ - public function record(Record $record): void - { - $this->builder->replaceRecord($this->id, $record); - } - - public function container(ContainerInterface $container): void - { - $this->builder->replaceContainer($this->id, $container); - } -} diff --git a/src/Setup/Exception/IntegrityConstraintException.php b/src/Setup/Exception/IntegrityConstraintException.php index d252e47..6f3fd63 100644 --- a/src/Setup/Exception/IntegrityConstraintException.php +++ b/src/Setup/Exception/IntegrityConstraintException.php @@ -16,9 +16,9 @@ class IntegrityConstraintException extends LogicException { - public static function alreadyDefined(string $resource): self + public static function alreadyDefined(string $id): self { - return new self("Cannot overwrite defined $resource"); + return new self("Cannot overwrite defined `$id` entry"); } public static function prefixConflict(string $prefix): self @@ -31,11 +31,6 @@ public static function unexpectedPrefixSeparator(string $separator, string $id): return new self("Container id cannot contain `$separator` separator - `$id` id given"); } - public static function undefined(string $id): self - { - return new self("Cannot change undefined `$id` Record"); - } - public static function missingReference(string $id) { return new self("Wrapped `$id` entry should be referenced by decorating object"); diff --git a/src/Setup/Exception/OverwriteRuleException.php b/src/Setup/Exception/OverwriteRuleException.php new file mode 100644 index 0000000..c7204d0 --- /dev/null +++ b/src/Setup/Exception/OverwriteRuleException.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Setup\Exception; + +use LogicException; + + +class OverwriteRuleException extends LogicException +{ + public static function alreadyDefined(string $id): self + { + return new self("Cannot overwrite defined `$id` entry"); + } + + public static function undefined(string $id): self + { + return new self("Cannot change undefined `$id` entry"); + } +} diff --git a/src/Setup/ValidatedSetup.php b/src/Setup/ValidatedSetup.php deleted file mode 100644 index 253a649..0000000 --- a/src/Setup/ValidatedSetup.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Setup; - -use Polymorphine\Container\Setup; -use Polymorphine\Container\Records; -use Polymorphine\Container\CompositeContainer; -use Psr\Container\ContainerInterface; - - -class ValidatedSetup extends Setup -{ - private $reservedIds = []; - - /** - * @param Records\Record[] $records - * @param ContainerInterface[] $containers - */ - public function __construct(array $records = [], array $containers = []) - { - parent::__construct($records, $containers); - $this->validateState(); - } - - public function addRecord(string $id, Records\Record $record): void - { - $this->checkRecordId($id); - if (isset($this->records[$id])) { - throw Exception\IntegrityConstraintException::alreadyDefined("`$id` record"); - } - $this->records[$id] = $record; - } - - public function addContainer(string $id, ContainerInterface $container): void - { - $this->checkContainerId($id); - if (isset($this->containers[$id])) { - throw Exception\IntegrityConstraintException::alreadyDefined("`$id` container"); - } - $this->containers[$id] = $container; - } - - public function replaceRecord(string $id, Records\Record $record): void - { - $this->checkRecordId($id); - if (!isset($this->records[$id])) { - throw Exception\IntegrityConstraintException::undefined("`$id` record"); - } - $this->records[$id] = $record; - } - - public function replaceContainer(string $id, ContainerInterface $container): void - { - $this->checkContainerId($id); - if (!isset($this->containers[$id])) { - throw Exception\IntegrityConstraintException::undefined("`$id` container"); - } - $this->containers[$id] = $container; - } - - protected function records(): Records - { - return new Records\TrackedRecords($this->records); - } - - private function validateState() - { - foreach ($this->records as $id => $record) { - $this->checkRecord($id, $record); - } - - foreach ($this->containers as $id => $container) { - $this->checkContainer($id, $container); - } - } - - private function checkRecord(string $id, $value): void - { - if (!$value instanceof Records\Record) { - throw Exception\InvalidTypeException::recordExpected($id); - } - $this->checkRecordId($id); - } - - private function checkRecordId(string $id): void - { - if (isset($this->containers[$id])) { - throw Exception\IntegrityConstraintException::alreadyDefined("`$id` container"); - } - - $separator = strpos($id, CompositeContainer::SEPARATOR); - $reserved = $separator === false ? $id : substr($id, 0, $separator); - if (isset($this->containers[$reserved])) { - throw Exception\IntegrityConstraintException::prefixConflict($reserved); - } - - $this->reservedIds[$reserved] = true; - } - - private function checkContainer(string $id, $value): void - { - if (!$value instanceof ContainerInterface) { - throw Exception\InvalidTypeException::containerExpected($id); - } - $this->checkContainerId($id); - } - - private function checkContainerId(string $id): void - { - if (strpos($id, CompositeContainer::SEPARATOR) !== false) { - throw Exception\IntegrityConstraintException::unexpectedPrefixSeparator(CompositeContainer::SEPARATOR, $id); - } - - if (isset($this->reservedIds[$id])) { - throw Exception\IntegrityConstraintException::alreadyDefined("`$id` record (or record prefix)"); - } - } -} diff --git a/src/Setup/Entry/Wrapper.php b/src/Setup/Wrapper.php similarity index 89% rename from src/Setup/Entry/Wrapper.php rename to src/Setup/Wrapper.php index 18dbd95..e0f83a8 100644 --- a/src/Setup/Entry/Wrapper.php +++ b/src/Setup/Wrapper.php @@ -9,11 +9,9 @@ * with this source code in the file LICENSE. */ -namespace Polymorphine\Container\Setup\Entry; +namespace Polymorphine\Container\Setup; -use Polymorphine\Container\Setup\Entry; use Polymorphine\Container\Records\Record; -use Polymorphine\Container\Setup\Exception; class Wrapper diff --git a/tests/Doubles/MockedBuild.php b/tests/Doubles/MockedBuild.php new file mode 100644 index 0000000..e87ec7a --- /dev/null +++ b/tests/Doubles/MockedBuild.php @@ -0,0 +1,74 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Tests\Doubles; + +use Polymorphine\Container\Records; +use Polymorphine\Container\Setup\Build; +use Polymorphine\Container\Setup\Entry; +use Polymorphine\Container\Setup\Wrapper; +use Psr\Container\ContainerInterface; + + +class MockedBuild extends Build +{ + public $container; + public $wrapper; + + public $setRecords = []; + public $setContainers = []; + + private $defined; + + public static function defined(): self + { + $build = new self(); + $build->defined = true; + return $build; + } + + public static function undefined(): self + { + $build = new self(); + $build->defined = false; + return $build; + } + + public function container(): ContainerInterface + { + return $this->container = new FakeContainer(); + } + + public function has(string $id): bool + { + return $this->defined ?? parent::has($id); + } + + public function decorator(string $id): Wrapper + { + return $this->wrapper = new Wrapper($id, new MockedRecord(), new Entry($id, $this)); + } + + public function setRecord(string $id, Records\Record $record): void + { + $this->setRecords[] = [$id, $record]; + } + + public function setContainer(string $id, ContainerInterface $container): void + { + $this->setContainers[] = [$id, $container]; + } + + protected function records(): Records + { + return new Records($this->records); + } +} diff --git a/tests/Doubles/MockedSetup.php b/tests/Doubles/MockedSetup.php deleted file mode 100644 index 02a1e50..0000000 --- a/tests/Doubles/MockedSetup.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Tests\Doubles; - -use Polymorphine\Container\Setup; -use Polymorphine\Container\Records; -use Psr\Container\ContainerInterface; - - -class MockedSetup extends Setup -{ - private $addedRecords = []; - private $replacedRecords = []; - private $addedContainers = []; - private $replacedContainers = []; - - private $replace; - - public static function added(): self - { - $object = new self(); - $object->replace = false; - return $object; - } - - public static function replaced(): self - { - $object = new self(); - $object->replace = true; - return $object; - } - - public function recordChanges(): array - { - return $this->replace ? $this->replacedRecords : $this->addedRecords; - } - - public function containerChanges(): array - { - return $this->replace ? $this->replacedContainers : $this->addedContainers; - } - - public function addRecord(string $id, Records\Record $record): void - { - $this->addedRecords[] = [$id, $record]; - } - - public function addContainer(string $id, ContainerInterface $container): void - { - $this->addedContainers[] = [$id, $container]; - } - - public function replaceRecord(string $id, Records\Record $record): void - { - $this->replacedRecords[] = [$id, $record]; - } - - public function replaceContainer(string $id, ContainerInterface $container): void - { - $this->replacedContainers[] = [$id, $container]; - } - - protected function records(): Records - { - return new Records(); - } -} diff --git a/tests/Setup/BasicSetupTest.php b/tests/Setup/BasicSetupTest.php deleted file mode 100644 index a71f70a..0000000 --- a/tests/Setup/BasicSetupTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Tests\Setup; - -use Polymorphine\Container\Tests\SetupTest; -use Polymorphine\Container\Setup; -use Polymorphine\Container\Records; - - -class BasicSetupTest extends SetupTest -{ - protected function builder(array $records = [], array $containers = []): Setup - { - return Setup::basic($records, $containers); - } - - protected function records(array $records = []): Records - { - return new Records($records); - } -} diff --git a/tests/Setup/Build/ValidatedBuildTest.php b/tests/Setup/Build/ValidatedBuildTest.php new file mode 100644 index 0000000..b0ca844 --- /dev/null +++ b/tests/Setup/Build/ValidatedBuildTest.php @@ -0,0 +1,64 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Tests\Setup\Build; + +use Polymorphine\Container\Tests\Setup\BuildTest; +use Polymorphine\Container\Tests\Fixtures\ExampleImpl; +use Polymorphine\Container\Tests\Doubles; +use Polymorphine\Container\Records; +use Polymorphine\Container\Setup; + + +class ValidatedBuildTest extends BuildTest +{ + public function testValidatedBuild_InstantiationWithInvalidRecordType_ThrowsException() + { + $this->expectException(Setup\Exception\InvalidTypeException::class); + $this->builder(['foo' => ExampleImpl::new()]); + } + + public function testValidatedBuild_InstantiationWithInvalidContainerType_ThrowsException() + { + $this->expectException(Setup\Exception\InvalidTypeException::class); + $this->builder([], ['foo' => ExampleImpl::new()]); + } + + public function testValidatedBuild_InvalidContainerId_ThrowsException() + { + $this->expectException(Setup\Exception\IntegrityConstraintException::class); + $this->builder([], ['foo.bar' => Doubles\FakeContainer::new()]); + } + + public function testValidatedBuild_setContainerWithIdTakenByRecordsPrefix_ThrowsException() + { + $setup = $this->builder(['foo.bar' => Doubles\MockedRecord::new()]); + $this->expectException(Setup\Exception\IntegrityConstraintException::class); + $setup->setContainer('foo', Doubles\FakeContainer::new()); + } + + public function testValidatedBuild_setRecordMethodWithDefinedContainerPrefix_ThrowsException() + { + $setup = $this->builder([], ['defined' => Doubles\FakeContainer::new()]); + $this->expectException(Setup\Exception\IntegrityConstraintException::class); + $setup->setRecord('defined.record', Doubles\MockedRecord::new()); + } + + protected function builder(array $records = [], array $containers = []): Setup\Build + { + return new Setup\Build\ValidatedBuild($records, $containers); + } + + protected function records(array $records = []): Records + { + return new Records\TrackedRecords($records); + } +} diff --git a/tests/Setup/BuildTest.php b/tests/Setup/BuildTest.php new file mode 100644 index 0000000..76f4104 --- /dev/null +++ b/tests/Setup/BuildTest.php @@ -0,0 +1,94 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Polymorphine\Container\Tests\Setup; + +use PHPUnit\Framework\TestCase; +use Polymorphine\Container\Records; +use Polymorphine\Container\Setup; +use Polymorphine\Container\RecordContainer; +use Polymorphine\Container\CompositeContainer; +use Polymorphine\Container\Tests\Doubles; + + +class BuildTest extends TestCase +{ + public function testBuild_container_ReturnsRecordContainerWithDefinedRecords() + { + $setup = $this->builder($records = ['foo' => Doubles\MockedRecord::new()]); + $this->assertEquals(new RecordContainer($this->records($records)), $setup->container()); + } + + public function testBuildWithSubContainers_container_ReturnsCompositeContainer() + { + $setup = $this->builder( + $records = ['foo' => Doubles\MockedRecord::new()], + $containers = ['bar' => Doubles\FakeContainer::new()] + ); + $this->assertEquals(new CompositeContainer($this->records($records), $containers), $setup->container()); + } + + public function testBuild_addRecord_WillCreateContainerWithAddedRecords() + { + $setup = $this->builder(); + $setup->setRecord('foo', $added = Doubles\MockedRecord::new('added')); + $this->assertSame('added', $setup->container()->get('foo')); + } + + public function testBuild_addContainer_WillCreateContainerWithAddedContainers() + { + $setup = $this->builder(); + $setup->setContainer('foo', $container = Doubles\FakeContainer::new()); + $this->assertSame($container, $setup->container()->get('foo')); + } + + public function testBuild_has_ReturnsTrueForDefinedIds() + { + $setup = $this->builder( + ['record' => Doubles\MockedRecord::new()], + ['container' => Doubles\FakeContainer::new()] + ); + $setup->setRecord('addedRecord', Doubles\MockedRecord::new()); + $setup->setContainer('addedContainer', Doubles\FakeContainer::new()); + + $this->assertTrue($setup->has('record')); + $this->assertTrue($setup->has('container')); + $this->assertTrue($setup->has('addedRecord')); + $this->assertTrue($setup->has('addedContainer')); + + $this->assertFalse($setup->has('undefined')); + $this->assertFalse($setup->has('Record')); + } + + public function testBuild_decoratorWithRecordId_ReturnsWrapperForGivenRecord() + { + $setup = $this->builder(['foo' => $record = Doubles\MockedRecord::new('not decorated')]); + $expected = new Setup\Wrapper('foo', $record, new Setup\Entry('foo', $setup)); + $this->assertEquals($expected, $setup->decorator('foo')); + } + + public function testBuild_decoratorForUndefinedRecord_ThrowsException() + { + $setup = $this->builder(['foo' => Doubles\MockedRecord::new('not decorated')]); + $this->expectException(Setup\Exception\OverwriteRuleException::class); + $setup->decorator('bar'); + } + + protected function builder(array $records = [], array $containers = []): Setup\Build + { + return new Setup\Build($records, $containers); + } + + protected function records(array $records = []): Records + { + return new Records($records); + } +} diff --git a/tests/Setup/Entry/AddEntryTest.php b/tests/Setup/Entry/AddEntryTest.php deleted file mode 100644 index bade6e9..0000000 --- a/tests/Setup/Entry/AddEntryTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Tests\Setup\Entry; - -use Polymorphine\Container\Tests\Setup\EntryTest; -use Polymorphine\Container\Setup\Entry; -use Polymorphine\Container\Tests\Doubles; - - -class AddEntryTest extends EntryTest -{ - protected function entry(string $id, Doubles\MockedSetup $setup = null): Entry - { - return new Entry\AddEntry($id, $setup ?? $this->builder()); - } - - protected function builder(): Doubles\MockedSetup - { - return Doubles\MockedSetup::added(); - } -} diff --git a/tests/Setup/Entry/ReplaceEntryTest.php b/tests/Setup/Entry/ReplaceEntryTest.php deleted file mode 100644 index 3b5249b..0000000 --- a/tests/Setup/Entry/ReplaceEntryTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Tests\Setup\Entry; - -use Polymorphine\Container\Tests\Setup\EntryTest; -use Polymorphine\Container\Setup\Entry; -use Polymorphine\Container\Tests\Doubles; - - -class ReplaceEntryTest extends EntryTest -{ - protected function entry(string $id, Doubles\MockedSetup $setup = null): Entry - { - return new Entry\ReplaceEntry($id, $setup ?? $this->builder()); - } - - protected function builder(): Doubles\MockedSetup - { - return Doubles\MockedSetup::replaced(); - } -} diff --git a/tests/Setup/EntryTest.php b/tests/Setup/EntryTest.php index e557140..88bec59 100644 --- a/tests/Setup/EntryTest.php +++ b/tests/Setup/EntryTest.php @@ -14,13 +14,12 @@ use PHPUnit\Framework\TestCase; use Polymorphine\Container\Setup\Entry; use Polymorphine\Container\Records\Record; -use Polymorphine\Container\Setup\Exception\IntegrityConstraintException; +use Polymorphine\Container\Setup\Exception; +use Polymorphine\Container\Tests\Fixtures; use Polymorphine\Container\Tests\Doubles; -use Polymorphine\Container\Tests\Fixtures\DecoratorExample; -use Polymorphine\Container\Tests\Fixtures\ExampleImpl; -abstract class EntryTest extends TestCase +class EntryTest extends TestCase { public function testInstantiation() { @@ -31,13 +30,13 @@ public function testEntry_record_ChangesSetupRecords() { $setup = $this->builder(); $entry = $this->entry('foo', $setup); - $this->assertSame([], $setup->recordChanges()); + $this->assertSame([], $setup->setRecords); $entry->record($fooRecord = Doubles\MockedRecord::new()); - $this->assertSame([['foo', $fooRecord]], $setup->recordChanges()); + $this->assertSame([['foo', $fooRecord]], $setup->setRecords); $entry = $this->entry('bar', $setup); $entry->record($barRecord = Doubles\MockedRecord::new()); - $this->assertSame([['foo', $fooRecord], ['bar', $barRecord]], $setup->recordChanges()); + $this->assertSame([['foo', $fooRecord], ['bar', $barRecord]], $setup->setRecords); } public function testEntry_value_SetsSetupValueRecord() @@ -47,7 +46,7 @@ public function testEntry_value_SetsSetupValueRecord() $entry->value('bar'); $record = new Record\ValueRecord('bar'); - $this->assertEquals([['foo', $record]], $setup->recordChanges()); + $this->assertEquals([['foo', $record]], $setup->setRecords); } public function testEntry_callback_SetsSetupValueRecord() @@ -55,12 +54,12 @@ public function testEntry_callback_SetsSetupValueRecord() $setup = $this->builder(); $entry = $this->entry('foo', $setup); - $object = ExampleImpl::new(); + $object = Fixtures\ExampleImpl::new(); $callback = function () use ($object) { return $object; }; $entry->callback($callback); $record = new Record\CallbackRecord($callback); - $this->assertEquals([['foo', $record]], $changes = $setup->recordChanges()); + $this->assertEquals([['foo', $record]], $changes = $setup->setRecords); /** @var Record $record */ $record = $changes[0][1]; @@ -72,9 +71,9 @@ public function testEntry_instance_SetsSetupValueRecord() $setup = $this->builder(); $entry = $this->entry('foo', $setup); - $entry->instance(ExampleImpl::class, 'foo', 'bar'); - $record = new Record\InstanceRecord(ExampleImpl::class, 'foo', 'bar'); - $this->assertEquals([['foo', $record]], $setup->recordChanges()); + $entry->instance(Fixtures\ExampleImpl::class, 'foo', 'bar'); + $record = new Record\InstanceRecord(Fixtures\ExampleImpl::class, 'foo', 'bar'); + $this->assertEquals([['foo', $record]], $setup->setRecords); } public function testEntry_product_SetsSetupValueRecord() @@ -84,7 +83,7 @@ public function testEntry_product_SetsSetupValueRecord() $entry->product('factory.id', 'create', 'bar', 'baz'); $record = new Record\ProductRecord('factory.id', 'create', 'bar', 'baz'); - $this->assertEquals([['foo', $record]], $setup->recordChanges()); + $this->assertEquals([['foo', $record]], $setup->setRecords); } public function testEntry_wrappedInstance_SetsSetupValueRecord() @@ -92,15 +91,15 @@ public function testEntry_wrappedInstance_SetsSetupValueRecord() $setup = $this->builder(); $entry = $this->entry('foo', $setup); - $entry->wrappedInstance(ExampleImpl::class, 'bar') - ->with(DecoratorExample::class, 'foo', 'baz') - ->with(DecoratorExample::class, 'qux', 'foo') + $entry->wrappedInstance(Fixtures\ExampleImpl::class, 'bar') + ->with(Fixtures\DecoratorExample::class, 'foo', 'baz') + ->with(Fixtures\DecoratorExample::class, 'qux', 'foo') ->compose(); - $record = new Record\ComposedInstanceRecord('foo', new Record\InstanceRecord(ExampleImpl::class, 'bar'), [ - [DecoratorExample::class, ['foo', 'baz']], - [DecoratorExample::class, ['qux', 'foo']] + $record = new Record\ComposedInstanceRecord('foo', new Record\InstanceRecord(Fixtures\ExampleImpl::class, 'bar'), [ + [Fixtures\DecoratorExample::class, ['foo', 'baz']], + [Fixtures\DecoratorExample::class, ['qux', 'foo']] ]); - $this->assertEquals([['foo', $record]], $setup->recordChanges()); + $this->assertEquals([['foo', $record]], $setup->setRecords); } public function testEntry_wrappedInstanceWithNoReferenceToWrappedObject_ThrowsException() @@ -108,9 +107,9 @@ public function testEntry_wrappedInstanceWithNoReferenceToWrappedObject_ThrowsEx $setup = $this->builder(); $entry = $this->entry('foo', $setup); - $wrapper = $entry->wrappedInstance(ExampleImpl::class, 'bar'); - $this->expectException(IntegrityConstraintException::class); - $wrapper->with(DecoratorExample::class, 'not-foo', 'baz'); + $wrapper = $entry->wrappedInstance(Fixtures\ExampleImpl::class, 'bar'); + $this->expectException(Exception\IntegrityConstraintException::class); + $wrapper->with(Fixtures\DecoratorExample::class, 'not-foo', 'baz'); } public function testEntry_container_SetsSetupContainer() @@ -118,20 +117,26 @@ public function testEntry_container_SetsSetupContainer() $setup = $this->builder(); $entry = $this->entry('foo', $setup); - $this->assertSame([], $setup->containerChanges()); + $this->assertSame([], $setup->setContainers); $fooContainer = Doubles\FakeContainer::new(); $entry->container($fooContainer); - $this->assertSame([['foo', $fooContainer]], $setup->containerChanges()); + $this->assertSame([['foo', $fooContainer]], $setup->setContainers); $entry = $this->entry('bar', $setup); $barContainer = Doubles\FakeContainer::new(); $entry->container($barContainer); - $this->assertSame([['foo', $fooContainer], ['bar', $barContainer]], $setup->containerChanges()); + $this->assertSame([['foo', $fooContainer], ['bar', $barContainer]], $setup->setContainers); } - abstract protected function builder(): Doubles\MockedSetup; + private function builder(): Doubles\MockedBuild + { + return new Doubles\MockedBuild(); + } - abstract protected function entry(string $name, Doubles\MockedSetup $setup = null): Entry; + private function entry(string $id, Doubles\MockedBuild $build = null): Entry + { + return new Entry($id, $build ?? $this->builder()); + } } diff --git a/tests/Setup/ValidatedSetupTest.php b/tests/Setup/ValidatedSetupTest.php deleted file mode 100644 index 6ef3177..0000000 --- a/tests/Setup/ValidatedSetupTest.php +++ /dev/null @@ -1,105 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Polymorphine\Container\Tests\Setup; - -use Polymorphine\Container\Tests\Fixtures\ExampleImpl; -use Polymorphine\Container\Tests\SetupTest; -use Polymorphine\Container\Setup; -use Polymorphine\Container\Records; -use Polymorphine\Container\Tests\Doubles; - - -class ValidatedSetupTest extends SetupTest -{ - public function testValidatedSetup_InstantiationWithInvalidRecordType_ThrowsException() - { - $this->expectException(Setup\Exception\InvalidTypeException::class); - $this->builder(['foo' => ExampleImpl::new()]); - } - - public function testValidatedSetup_InstantiationWithInvalidContainerType_ThrowsException() - { - $this->expectException(Setup\Exception\InvalidTypeException::class); - $this->builder([], ['foo' => ExampleImpl::new()]); - } - - public function testValidatedSetup_InvalidContainerId_ThrowsException() - { - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $this->builder([], ['foo.bar' => Doubles\FakeContainer::new()]); - } - - public function testValidatedSetup_ContainerIdTakenByRecord_ThrowsException() - { - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $this->builder(['foo' => Doubles\MockedRecord::new()], ['foo' => Doubles\FakeContainer::new()]); - } - - public function testValidatedSetup_addRecordMethodForDefinedRecord_ThrowsException() - { - $setup = $this->builder(['defined' => Doubles\MockedRecord::new()]); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->addRecord('defined', Doubles\MockedRecord::new()); - } - - public function testValidatedSetup_addContainerMethodForDefinedContainer_ThrowsException() - { - $setup = $this->builder([], ['defined' => Doubles\FakeContainer::new()]); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->addContainer('defined', Doubles\FakeContainer::new()); - } - - public function testValidatedSetup_addRecordMethodWithDefinedContainerId_ThrowsException() - { - $setup = $this->builder([], ['defined' => Doubles\FakeContainer::new()]); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->addRecord('defined', Doubles\MockedRecord::new()); - } - - public function testValidatedSetup_addContainerWithIdTakenByRecordsPrefix_ThrowsException() - { - $setup = $this->builder(['foo.bar' => Doubles\MockedRecord::new()]); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->addContainer('foo', Doubles\FakeContainer::new()); - } - - public function testValidatedSetup_addRecordMethodWithDefinedContainerPrefix_ThrowsException() - { - $setup = $this->builder([], ['defined' => Doubles\FakeContainer::new()]); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->addRecord('defined.record', Doubles\MockedRecord::new()); - } - - public function testValidatedSetup_replaceRecordMethodForUndefinedRecord_ThrowsException() - { - $setup = $this->builder(); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->replaceRecord('undefined', Doubles\MockedRecord::new()); - } - - public function testValidatedSetup_replaceContainerMethodForUndefinedContainer_ThrowsException() - { - $setup = $this->builder(); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->replaceContainer('undefined', Doubles\FakeContainer::new()); - } - - protected function builder(array $records = [], array $containers = []): Setup - { - return Setup::validated($records, $containers); - } - - protected function records(array $records = []): Records - { - return new Records\TrackedRecords($records); - } -} diff --git a/tests/SetupTest.php b/tests/SetupTest.php index 698840b..c413bbc 100644 --- a/tests/SetupTest.php +++ b/tests/SetupTest.php @@ -13,110 +13,75 @@ use PHPUnit\Framework\TestCase; use Polymorphine\Container\Setup; -use Polymorphine\Container\Records; -use Polymorphine\Container\RecordContainer; -use Polymorphine\Container\CompositeContainer; -abstract class SetupTest extends TestCase +class SetupTest extends TestCase { public function testInstantiation() { $this->assertInstanceOf(Setup::class, $this->builder()); + $this->assertInstanceOf(Setup::class, Setup::production()); + $this->assertInstanceOf(Setup::class, Setup::development()); } - public function testSetup_container_ReturnsRecordContainerWithRecords() + public function testSetup_container_ReturnsContainerFromBuild() { - $records = ['foo' => Doubles\MockedRecord::new()]; - - $setup = $this->builder($records); - $expected = new RecordContainer($this->records($records)); - $this->assertEquals($expected, $setup->container()); + $setup = $this->builder($build); + $this->assertSame($setup->container(), $build->container); } - public function testSetupWithSubContainers_container_ReturnsCompositeContainer() + public function testSetup_setUndefinedId_ReturnsEntryObject() { - $records = ['foo' => Doubles\MockedRecord::new()]; - $containers = ['bar' => Doubles\FakeContainer::new()]; - - $setup = $this->builder($records, $containers); - $expected = new CompositeContainer($this->records($records), $containers); - $this->assertEquals($expected, $setup->container()); + $setup = new Setup($build = Doubles\MockedBuild::undefined()); + $expected = new Setup\Entry('foo', $build); + $this->assertEquals($expected, $setup->set('foo')); } - public function testSetup_addRecords_WillCreateContainerWithAddedRecords() + public function testSetup_setDefinedId_ThrowsException() { - $records = ['foo' => Doubles\MockedRecord::new(), 'bar' => Doubles\MockedRecord::new()]; - - $setup = $this->builder(); - $setup->addRecords($records); - $expected = new RecordContainer($this->records($records)); - $this->assertEquals($expected, $setup->container()); - } - - public function testSetup_replaceRecord_WillCreateContainerWithReplacedRecords() - { - $records = ['foo' => Doubles\MockedRecord::new('original')]; - $replaced = Doubles\MockedRecord::new('replaced'); - - $setup = $this->builder($records); - $setup->replaceRecord('foo', $replaced); - $expected = new RecordContainer($this->records(['foo' => $replaced])); - $this->assertEquals($expected, $setup->container()); + $setup = new Setup(Doubles\MockedBuild::defined()); + $this->expectException(Setup\Exception\OverwriteRuleException::class); + $setup->set('foo'); } - public function testSetup_addContainer_WillCreateContainerWithAddedContainers() + public function testSetup_replaceDefinedId_ReturnsEntryObject() { - $container = Doubles\FakeContainer::new(); - - $setup = $this->builder(); - $setup->addContainer('test', $container); - $expected = new CompositeContainer($this->records([]), ['test' => $container]); - $this->assertEquals($expected, $setup->container()); + $setup = new Setup($build = Doubles\MockedBuild::defined()); + $expected = new Setup\Entry('foo', $build); + $this->assertEquals($expected, $setup->replace('foo')); } - public function testSetup_replaceContainer_WillCreateContainerWithReplacedContainers() + public function testSetup_replaceUndefinedId_ThrowsException() { - $original = Doubles\FakeContainer::new(['A' => 'original']); - $replaced = Doubles\FakeContainer::new(['A' => 'replaced']); - - $setup = $this->builder([], ['foo' => $original]); - $setup->replaceContainer('foo', $replaced); - $expected = new CompositeContainer($this->records(), ['foo' => $replaced]); - $this->assertEquals($expected, $setup->container()); + $setup = new Setup(Doubles\MockedBuild::undefined()); + $this->expectException(Setup\Exception\OverwriteRuleException::class); + $setup->replace('foo'); } - public function testSetup_add_ReturnsAddEntryObject() + public function testSetup_fallbackForDefinedId_ReturnsInactiveEntryObject() { - $setup = $this->builder(); - $expected = new Setup\Entry\AddEntry('foo', $setup); - $this->assertEquals($expected, $setup->add('foo')); + $setup = new Setup($build = Doubles\MockedBuild::defined()); + $expected = new Setup\Entry('foo', $build); + $this->assertNotEquals($expected, $entry = $setup->fallback('foo')); + $this->assertInstanceOf(Setup\Entry::class, $entry); } - public function testSetup_replace_ReturnsReplaceEntryObject() + public function testSetup_fallbackForUndefinedId_ReturnsEntryObject() { - $setup = $this->builder(); - $expected = new Setup\Entry\ReplaceEntry('foo', $setup); - $this->assertEquals($expected, $setup->replace('foo')); + $setup = new Setup($build = Doubles\MockedBuild::undefined()); + $expected = new Setup\Entry('foo', $build); + $this->assertEquals($expected, $setup->fallback('foo')); } public function testSetup_decorate_ReturnsReplacingWrapper() { - $decorated = Doubles\MockedRecord::new('decorated'); - - $setup = $this->builder(['foo' => $decorated]); - $expected = new Setup\Entry\Wrapper('foo', $decorated, new Setup\Entry\ReplaceEntry('foo', $setup)); - $this->assertEquals($expected, $setup->decorate('foo')); + $setup = $this->builder($build); + $this->assertEquals($setup->decorate('foo'), $build->wrapper); } - public function testSetup_decorateUndefinedRecord_ThrowsException() + private function builder(?Setup\Build &$build = null): Setup { - $setup = $this->builder(['foo' => Doubles\MockedRecord::new('not decorated')]); - $this->expectException(Setup\Exception\IntegrityConstraintException::class); - $setup->decorate('bar'); + $build = new Doubles\MockedBuild(); + return new Setup($build); } - - abstract protected function builder(array $records = [], array $containers = []): Setup; - - abstract protected function records(array $records = []): Records; }