From 7ea68b63f82f218656c9aca735d41b4b41f8a8da Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Tue, 13 Feb 2024 23:00:43 +0700 Subject: [PATCH 1/8] Support parameters name binding --- src/DefinitionStorage.php | 27 ++++++++++++++++++++++++--- src/Helpers/Normalizer.php | 4 ++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index 0cafb62..9a1ab4c 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -96,12 +96,33 @@ public function set(string $id, mixed $definition): void * * @throws CircularReferenceException */ - private function isResolvable(string $id, array $building): bool + private function isResolvable(string $id, array $building, ?string $parameterName = null): bool { if (isset($this->definitions[$id])) { return true; } + if ( + $parameterName !== null + && isset($this->definitions[$id . '$'.$parameterName]) + ) { + $buildingClass = array_key_last($building); + $definition = $this->definitions[$buildingClass] ?? null; + $temporaryDefinition = ArrayDefinition::fromConfig([ + \Yiisoft\Definitions\ArrayDefinition::CLASS_NAME => $buildingClass, + \Yiisoft\Definitions\ArrayDefinition::CONSTRUCTOR => [ + $parameterName => Reference::to($this->definitions[$id . '$' . $parameterName]) + ] + ]); + if ($definition instanceof ArrayDefinition) { + $this->definitions[$buildingClass] = $definition->merge($temporaryDefinition); + } else { + $this->definitions[$buildingClass] = $temporaryDefinition; + } + + return true; + } + if ($this->useStrictMode || !class_exists($id)) { $this->buildStack += $building + [$id => 1]; return false; @@ -210,7 +231,7 @@ private function isResolvable(string $id, array $building): bool } if ( - !$this->isResolvable($typeName, $building) + !$this->isResolvable($typeName, $building, $parameter->getName()) && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName)) ) { $isResolvable = false; @@ -222,7 +243,7 @@ private function isResolvable(string $id, array $building): bool $this->buildStack += $building; } - if ($isResolvable) { + if ($isResolvable && !isset($this->definitions[$id])) { $this->definitions[$id] = $id; } diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 6747deb..b670f96 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -53,6 +53,10 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return $definition; } + if ($definition instanceof DefinitionInterface) { + return $definition; + } + if (is_string($definition)) { // Current class if ( From 28ca7014c8ee7c07aac18d6dc4efaa462f0094cb Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 13 Feb 2024 16:01:06 +0000 Subject: [PATCH 2/8] Apply fixes from StyleCI --- src/DefinitionStorage.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index 9a1ab4c..aacbaf7 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -104,15 +104,15 @@ private function isResolvable(string $id, array $building, ?string $parameterNam if ( $parameterName !== null - && isset($this->definitions[$id . '$'.$parameterName]) + && isset($this->definitions[$id . '$' . $parameterName]) ) { $buildingClass = array_key_last($building); $definition = $this->definitions[$buildingClass] ?? null; $temporaryDefinition = ArrayDefinition::fromConfig([ - \Yiisoft\Definitions\ArrayDefinition::CLASS_NAME => $buildingClass, - \Yiisoft\Definitions\ArrayDefinition::CONSTRUCTOR => [ - $parameterName => Reference::to($this->definitions[$id . '$' . $parameterName]) - ] + ArrayDefinition::CLASS_NAME => $buildingClass, + ArrayDefinition::CONSTRUCTOR => [ + $parameterName => Reference::to($this->definitions[$id . '$' . $parameterName]), + ], ]); if ($definition instanceof ArrayDefinition) { $this->definitions[$buildingClass] = $definition->merge($temporaryDefinition); From 8e2527c85509643fb98072497896ed1285fd6f6d Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Thu, 15 Feb 2024 23:26:32 +0700 Subject: [PATCH 3/8] Allow inject untyped services and union types --- src/DefinitionStorage.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index 9a1ab4c..06a0a0d 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -104,15 +104,18 @@ private function isResolvable(string $id, array $building, ?string $parameterNam if ( $parameterName !== null - && isset($this->definitions[$id . '$'.$parameterName]) + && ( + isset($this->definitions[$typedParameterName = $id . ' $' . $parameterName]) + || isset($this->definitions[$typedParameterName = '$' . $parameterName]) + ) ) { $buildingClass = array_key_last($building); $definition = $this->definitions[$buildingClass] ?? null; $temporaryDefinition = ArrayDefinition::fromConfig([ - \Yiisoft\Definitions\ArrayDefinition::CLASS_NAME => $buildingClass, - \Yiisoft\Definitions\ArrayDefinition::CONSTRUCTOR => [ - $parameterName => Reference::to($this->definitions[$id . '$' . $parameterName]) - ] + ArrayDefinition::CLASS_NAME => $buildingClass, + ArrayDefinition::CONSTRUCTOR => [ + $parameterName => Reference::to($this->definitions[$typedParameterName]), + ], ]); if ($definition instanceof ArrayDefinition) { $this->definitions[$buildingClass] = $definition->merge($temporaryDefinition); @@ -187,7 +190,7 @@ private function isResolvable(string $id, array $building, ?string $parameterNam continue; } $unionTypes[] = $typeName; - if ($this->isResolvable($typeName, $building)) { + if ($this->isResolvable($typeName, $building, $parameter->getName())) { $isUnionTypeResolvable = true; /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */ break; From 861988d3027c0bd2abd8dc4df7706975fa56dce9 Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Fri, 23 Feb 2024 14:32:28 +0700 Subject: [PATCH 4/8] Add docs --- CHANGELOG.md | 1 + README.md | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eee0a7d..4bc18ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.2.1 under development - Bug #86: Fix crash when intersection types are used (@vjik) +- Enh #87: Support parameter name bindings (@xepozz) ## 3.2.0 February 12, 2023 diff --git a/README.md b/README.md index 0084053..40675fb 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ at the moment of obtaining a service instance or creating an object. #### `ArrayDefinition` -Array definition allows describing a service or an object declaratively: +`ArrayDefinition` describes a class declarative: ```php use \Yiisoft\Definitions\ArrayDefinition; @@ -197,6 +197,38 @@ ContentNegotiator::class => [ ], ``` +### Name binding + +Name binding is a way to bind a name to a definition. It is used to resolve a definition not by its class name but by a name. + +Set a definitions with a specific name. It may be typed or untyped reference like: +1. `'$serviceName' => $definition` +2. `Service::class . ' $serviceName' => $definition` + +```php +return [ + '$fileCache' => FileCache::class, // implements CacheInterface + '$redisCache' => RedisCache::class, // implements CacheInterface + CacheInterface::class . ' $memCache' => MemCache::class, // also implements CacheInterface +] +``` + +So now you can resolve a definition by its name: + +```php +class MyService +{ + public function __construct( + CacheInterface $memCache, // typed reference + $fileCache, // untyped reference + CacheInterface $redisCache, // typed reference to untyped definition + ) { + // ... + } + +} +``` + ### Definition storage Definition storage could be used to hold and obtain definitions and check if a certain definition could be instantiated. From 679246c9c737f0ef0d3a2f8217a40ce7d28846d7 Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Fri, 23 Feb 2024 14:38:23 +0700 Subject: [PATCH 5/8] Fix psalm --- src/DefinitionStorage.php | 2 +- src/Helpers/Normalizer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index 395861e..e79a0b7 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -108,8 +108,8 @@ private function isResolvable(string $id, array $building, ?string $parameterNam isset($this->definitions[$typedParameterName = $id . ' $' . $parameterName]) || isset($this->definitions[$typedParameterName = '$' . $parameterName]) ) + && (!empty($buildingClass = array_key_last($building))) && class_exists($buildingClass) ) { - $buildingClass = array_key_last($building); $definition = $this->definitions[$buildingClass] ?? null; $temporaryDefinition = ArrayDefinition::fromConfig([ ArrayDefinition::CLASS_NAME => $buildingClass, diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index b670f96..7776865 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -92,7 +92,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi } // Ready object - if (is_object($definition) && !($definition instanceof DefinitionInterface)) { + if (is_object($definition)) { return new ValueDefinition($definition); } From cb0b647d46418c70a5b5cec9ade1c968422350a4 Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Fri, 23 Feb 2024 14:51:37 +0700 Subject: [PATCH 6/8] Add tests --- tests/Unit/DefinitionStorageTest.php | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Unit/DefinitionStorageTest.php b/tests/Unit/DefinitionStorageTest.php index 54f2a1d..0435774 100644 --- a/tests/Unit/DefinitionStorageTest.php +++ b/tests/Unit/DefinitionStorageTest.php @@ -9,6 +9,7 @@ use RuntimeException; use Yiisoft\Definitions\DefinitionStorage; use Yiisoft\Definitions\Exception\CircularReferenceException; +use Yiisoft\Definitions\Reference; use Yiisoft\Definitions\Tests\FunBike; use Yiisoft\Definitions\Tests\Support\Bike; use Yiisoft\Definitions\Tests\Support\Car; @@ -75,6 +76,46 @@ public function testExplicitDefinitionIsNotChecked(): void $this->assertSame([], $storage->getBuildStack()); } + public static function dataParameterNameBindings(): iterable + { + yield 'untyped reference' => [ + [ + '$engine' => new EngineMarkOne(), + '$color' => 'red', + Bike::class => Bike::class, + ], + Bike::class, + ]; + + yield 'typed reference' => [ + [ + EngineInterface::class . ' $engine' => new EngineMarkOne(), + ColorInterface::class . ' $color' => new ColorPink(), + Bike::class => Bike::class, + ], + Bike::class, + ]; + + yield 'referenced reference' => [ + [ + EngineInterface::class . ' $engine' => new EngineMarkOne(), + ColorInterface::class . ' $color' => Reference::to(ColorPink::class), + ColorPink::class => ColorPink::class, + Bike::class => Bike::class, + ], + Bike::class, + ]; + } + + /** + * @dataProvider dataParameterNameBindings + */ + public function testParameterNameBindings(array $definitions, string $class): void + { + $storage = new DefinitionStorage($definitions); + $this->assertTrue($storage->has($class)); + } + public function testNonExistingService(): void { $storage = new DefinitionStorage([]); From e1d258ebf49bbebb0426727318b3d33e95eaf954 Mon Sep 17 00:00:00 2001 From: Dmitrii Derepko Date: Sun, 3 Mar 2024 09:46:13 +0700 Subject: [PATCH 7/8] Add tests --- src/DefinitionStorage.php | 4 +- tests/Support/Helper/ReferenceResolver.php | 42 ++++++++ tests/Unit/ArrayDefinitionTest.php | 110 +++++++++++++++++++++ tests/Unit/DefinitionStorageTest.php | 43 +------- 4 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 tests/Support/Helper/ReferenceResolver.php diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index e79a0b7..3fae223 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -114,7 +114,9 @@ private function isResolvable(string $id, array $building, ?string $parameterNam $temporaryDefinition = ArrayDefinition::fromConfig([ ArrayDefinition::CLASS_NAME => $buildingClass, ArrayDefinition::CONSTRUCTOR => [ - $parameterName => Reference::to($this->definitions[$typedParameterName]), + $parameterName => is_string($this->definitions[$typedParameterName]) + ? Reference::to($this->definitions[$typedParameterName]) + : $this->definitions[$typedParameterName], ], ]); if ($definition instanceof ArrayDefinition) { diff --git a/tests/Support/Helper/ReferenceResolver.php b/tests/Support/Helper/ReferenceResolver.php new file mode 100644 index 0000000..2e9e033 --- /dev/null +++ b/tests/Support/Helper/ReferenceResolver.php @@ -0,0 +1,42 @@ +mockObject ??= new stdClass(); + } + + public function get(string $id) + { + $this->reference = $id; + $this->references[] = $id; + + return $this->mockObject; + } + + public function has(string $id): bool + { + return true; + } + + public function getReference(): ?string + { + return $this->reference; + } + + public function getReferences(): array + { + return $this->references; + } +} diff --git a/tests/Unit/ArrayDefinitionTest.php b/tests/Unit/ArrayDefinitionTest.php index 796b057..cbae230 100644 --- a/tests/Unit/ArrayDefinitionTest.php +++ b/tests/Unit/ArrayDefinitionTest.php @@ -7,14 +7,17 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Yiisoft\Definitions\ArrayDefinition; +use Yiisoft\Definitions\DefinitionStorage; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Reference; +use Yiisoft\Definitions\Tests\Support\Bike; use Yiisoft\Definitions\Tests\Support\Car; use Yiisoft\Definitions\Tests\Support\ColorInterface; use Yiisoft\Definitions\Tests\Support\ColorPink; use Yiisoft\Definitions\Tests\Support\EngineInterface; use Yiisoft\Definitions\Tests\Support\EngineMarkOne; use Yiisoft\Definitions\Tests\Support\EngineMarkTwo; +use Yiisoft\Definitions\Tests\Support\Helper\ReferenceResolver; use Yiisoft\Definitions\Tests\Support\Mouse; use Yiisoft\Definitions\Tests\Support\Phone; use Yiisoft\Definitions\Tests\Support\Recorder; @@ -523,4 +526,111 @@ public function testMagicMethods(): void $object->getEvents() ); } + + public static function dataParameterNameBindings(): iterable + { + yield 'untyped reference' => [ + [ + '$engine' => EngineMarkOne::class, + '$color' => 'red', + ], + Bike::class, + ]; + + yield 'typed reference' => [ + [ + EngineInterface::class . ' $engine' => EngineMarkOne::class, + ColorInterface::class . ' $color' => ColorPink::class, + ], + Bike::class, + ]; + + yield 'referenced reference' => [ + [ + EngineInterface::class . ' $engine' => EngineMarkOne::class, + ColorInterface::class . ' $color' => Reference::to(ColorPink::class), + ColorPink::class => ColorPink::class, + ], + Bike::class, + ]; + } + + /** + * @dataProvider dataParameterNameBindings + */ + public function testParameterNameBindingsFormat(array $definitions, string $class): void + { + $storage = new DefinitionStorage($definitions); + $this->assertInstanceOf(ArrayDefinition::class, $storage->get($class)); + } + + public function testParameterNameBindings(): void + { + $storage = new DefinitionStorage([ + '$engine' => EngineMarkOne::class, + ColorInterface::class . ' $color' => ColorPink::class, + ]); + + $object = $storage->get(Bike::class); + + $this->assertInstanceOf(ArrayDefinition::class, $object); + $this->assertEmpty($object->getMethodsAndProperties()); + $this->assertCount(2, $object->getConstructorArguments()); + $this->assertArrayHasKey('engine', $object->getConstructorArguments()); + $this->assertArrayHasKey('color', $object->getConstructorArguments()); + + $this->assertInstanceOf(Reference::class, $object->getConstructorArguments()['engine']); + $this->assertInstanceOf(Reference::class, $object->getConstructorArguments()['color']); + + $resolver = new ReferenceResolver(); + $object->getConstructorArguments()['engine']->resolve($resolver); + + $this->assertSame(EngineMarkOne::class, $resolver->getReference()); + + $object->getConstructorArguments()['color']->resolve($resolver); + $this->assertSame(ColorPink::class, $resolver->getReference()); + } + + public static function dataWrongParameterNameBindingsFormat(): iterable + { + yield 'reference without dollar' => [ + [ + 'engine' => EngineMarkOne::class, + '$color' => 'red', + ], + Bike::class, + ]; + + yield 'missing whitespace between class and variable' => [ + [ + EngineInterface::class . '$engine' => EngineMarkOne::class, + ColorInterface::class . ' $color' => ColorPink::class, + ], + Bike::class, + ]; + + yield 'missing definition' => [ + [ + EngineInterface::class . ' $engine' => EngineMarkOne::class, + ColorPink::class => ColorPink::class, + ], + Bike::class, + ]; + } + + /** + * @dataProvider dataWrongParameterNameBindingsFormat + */ + public function testWrongParameterNameBindingsFormat(array $definitions, string $class): void + { + $storage = new DefinitionStorage($definitions); + + $this->expectExceptionMessage( + sprintf( + 'Service %s doesn\'t exist in DefinitionStorage.', + $class, + ) + ); + $storage->get($class); + } } diff --git a/tests/Unit/DefinitionStorageTest.php b/tests/Unit/DefinitionStorageTest.php index 0435774..e59e07f 100644 --- a/tests/Unit/DefinitionStorageTest.php +++ b/tests/Unit/DefinitionStorageTest.php @@ -9,7 +9,6 @@ use RuntimeException; use Yiisoft\Definitions\DefinitionStorage; use Yiisoft\Definitions\Exception\CircularReferenceException; -use Yiisoft\Definitions\Reference; use Yiisoft\Definitions\Tests\FunBike; use Yiisoft\Definitions\Tests\Support\Bike; use Yiisoft\Definitions\Tests\Support\Car; @@ -18,8 +17,8 @@ use Yiisoft\Definitions\Tests\Support\ColorInterface; use Yiisoft\Definitions\Tests\Support\ColorPink; use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithBuiltinTypeWithoutDefault; -use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonExistingSubDependency; use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonExistingDependency; +use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonExistingSubDependency; use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithNonResolvableUnionTypes; use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithPrivateConstructor; use Yiisoft\Definitions\Tests\Support\DefinitionStorage\ServiceWithPrivateConstructorSubDependency; @@ -76,46 +75,6 @@ public function testExplicitDefinitionIsNotChecked(): void $this->assertSame([], $storage->getBuildStack()); } - public static function dataParameterNameBindings(): iterable - { - yield 'untyped reference' => [ - [ - '$engine' => new EngineMarkOne(), - '$color' => 'red', - Bike::class => Bike::class, - ], - Bike::class, - ]; - - yield 'typed reference' => [ - [ - EngineInterface::class . ' $engine' => new EngineMarkOne(), - ColorInterface::class . ' $color' => new ColorPink(), - Bike::class => Bike::class, - ], - Bike::class, - ]; - - yield 'referenced reference' => [ - [ - EngineInterface::class . ' $engine' => new EngineMarkOne(), - ColorInterface::class . ' $color' => Reference::to(ColorPink::class), - ColorPink::class => ColorPink::class, - Bike::class => Bike::class, - ], - Bike::class, - ]; - } - - /** - * @dataProvider dataParameterNameBindings - */ - public function testParameterNameBindings(array $definitions, string $class): void - { - $storage = new DefinitionStorage($definitions); - $this->assertTrue($storage->has($class)); - } - public function testNonExistingService(): void { $storage = new DefinitionStorage([]); From 0e2312cafc809d136a9abb3c89b3492d8ac24ea3 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sun, 3 Mar 2024 09:56:17 +0700 Subject: [PATCH 8/8] Update README.md Co-authored-by: Alexander Makarov --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 40675fb..9cbf8db 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ at the moment of obtaining a service instance or creating an object. #### `ArrayDefinition` -`ArrayDefinition` describes a class declarative: +`ArrayDefinition` describes a class declaratively: ```php use \Yiisoft\Definitions\ArrayDefinition;