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..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` -Array definition allows describing a service or an object declaratively: +`ArrayDefinition` describes a class declaratively: ```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. diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index c9e0459..3fae223 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -96,12 +96,38 @@ 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[$typedParameterName = $id . ' $' . $parameterName]) + || isset($this->definitions[$typedParameterName = '$' . $parameterName]) + ) + && (!empty($buildingClass = array_key_last($building))) && class_exists($buildingClass) + ) { + $definition = $this->definitions[$buildingClass] ?? null; + $temporaryDefinition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => $buildingClass, + ArrayDefinition::CONSTRUCTOR => [ + $parameterName => is_string($this->definitions[$typedParameterName]) + ? Reference::to($this->definitions[$typedParameterName]) + : $this->definitions[$typedParameterName], + ], + ]); + 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; @@ -172,7 +198,7 @@ private function isResolvable(string $id, array $building): bool 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; @@ -215,7 +241,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; @@ -227,7 +253,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..7776865 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 ( @@ -88,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); } 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 54f2a1d..e59e07f 100644 --- a/tests/Unit/DefinitionStorageTest.php +++ b/tests/Unit/DefinitionStorageTest.php @@ -17,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;