diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d7a3be..5c61153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.1.1 under development -- no changes in this release. +- Enh #44: In methods of array definitions add autowiring and improve variadic arguments support (@vjik) ## 2.1.0 October 25, 2022 diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index ebdaf9a..22aa041 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -6,7 +6,9 @@ use InvalidArgumentException; use Psr\Container\ContainerInterface; +use ReflectionMethod; use Yiisoft\Definitions\Contract\DefinitionInterface; +use Yiisoft\Definitions\Contract\ReferenceInterface; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Helpers\DefinitionExtractor; use Yiisoft\Definitions\Helpers\DefinitionResolver; @@ -137,32 +139,35 @@ public function getMethodsAndProperties(): array public function resolve(ContainerInterface $container): object { - $class = $this->getClass(); - $dependencies = DefinitionExtractor::fromClassName($class); - $constructorArguments = $this->getConstructorArguments(); + $class = $this->class; - $this->injectArguments($dependencies, $constructorArguments); - - /** @psalm-suppress MixedArgumentTypeCoercion */ - $resolved = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies); + $resolvedConstructorArguments = $this->resolveFunctionArguments( + $container, + DefinitionExtractor::fromClassName($class), + $this->getConstructorArguments() + ); /** @psalm-suppress MixedMethodCall */ - $object = new $class(...array_values($resolved)); + $object = new $class(...$resolvedConstructorArguments); foreach ($this->getMethodsAndProperties() as $item) { /** @var mixed $value */ [$type, $name, $value] = $item; - /** @var mixed */ - $value = DefinitionResolver::resolve($container, $this->referenceContainer, $value); if ($type === self::TYPE_METHOD) { - /** @var mixed */ - $setter = call_user_func_array([$object, $name], $value); + /** @var array $value */ + $resolvedMethodArguments = $this->resolveFunctionArguments( + $container, + DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)), + $value, + ); + /** @var mixed $setter */ + $setter = call_user_func_array([$object, $name], $resolvedMethodArguments); if ($setter instanceof $object) { - /** @var object */ + /** @var object $object */ $object = $setter; } } elseif ($type === self::TYPE_PROPERTY) { - $object->$name = $value; + $object->$name = DefinitionResolver::resolve($container, $this->referenceContainer, $value); } } @@ -170,17 +175,20 @@ public function resolve(ContainerInterface $container): object } /** - * @psalm-param array $dependencies - * @psalm-param-out array $dependencies + * @param array $dependencies * - * @throws InvalidConfigException + * @psalm-return list */ - private function injectArguments(array &$dependencies, array $arguments): void - { + private function resolveFunctionArguments( + ContainerInterface $container, + array $dependencies, + array $arguments + ): array { $isIntegerIndexed = $this->isIntegerIndexed($arguments); $dependencyIndex = 0; $usedArguments = []; $variadicKey = null; + foreach ($dependencies as $key => &$value) { if ($value->isVariadic()) { $variadicKey = $key; @@ -193,24 +201,41 @@ private function injectArguments(array &$dependencies, array $arguments): void $dependencyIndex++; } unset($value); + if ($variadicKey !== null) { if (!$isIntegerIndexed && isset($arguments[$variadicKey])) { + if ($arguments[$variadicKey] instanceof ReferenceInterface) { + /** @var mixed */ + $arguments[$variadicKey] = DefinitionResolver::resolve( + $container, + $this->referenceContainer, + $arguments[$variadicKey] + ); + } + if (is_array($arguments[$variadicKey])) { unset($dependencies[$variadicKey]); $dependencies += $arguments[$variadicKey]; - return; + } else { + throw new InvalidArgumentException( + sprintf( + 'Named argument for a variadic parameter should be an array, "%s" given.', + gettype($arguments[$variadicKey]) + ) + ); } - - throw new InvalidArgumentException(sprintf('Named argument for a variadic parameter should be an array, "%s" given.', gettype($arguments[$variadicKey]))); - } - - /** @var mixed $value */ - foreach ($arguments as $index => $value) { - if (!isset($usedArguments[$index])) { - $dependencies[$index] = DefinitionResolver::ensureResolvable($value); + } else { + /** @var mixed $value */ + foreach ($arguments as $index => $value) { + if (!isset($usedArguments[$index])) { + $dependencies[$index] = DefinitionResolver::ensureResolvable($value); + } } } } + + $resolvedArguments = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies); + return array_values($resolvedArguments); } /** diff --git a/src/Helpers/DefinitionResolver.php b/src/Helpers/DefinitionResolver.php index 5a67ca7..b2e3ed0 100644 --- a/src/Helpers/DefinitionResolver.php +++ b/src/Helpers/DefinitionResolver.php @@ -23,7 +23,7 @@ final class DefinitionResolver * * @param ContainerInterface $container Container to get dependencies from. * @param ContainerInterface|null $referenceContainer Container to get references from. - * @psalm-param array $definitions Definitions to resolve. + * @param array $definitions Definitions to resolve. * * @return array The resolved dependencies. */ @@ -65,7 +65,6 @@ public static function resolve(ContainerInterface $container, ?ContainerInterfac /** @var mixed $definition */ $definition = $definition->resolve($container); } elseif (is_array($definition)) { - /** @psalm-var array $definition */ return self::resolveArray($container, $referenceContainer, $definition); } diff --git a/tests/Support/Mouse.php b/tests/Support/Mouse.php new file mode 100644 index 0000000..2fbe872 --- /dev/null +++ b/tests/Support/Mouse.php @@ -0,0 +1,39 @@ +name = $name; + $this->engine = $engine; + } + + public function setNameAndColors(string $name, ...$colors): void + { + $this->name = $name; + $this->colors = $colors; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getEngine(): ?EngineInterface + { + return $this->engine; + } + + public function getColors(): array + { + return $this->colors; + } +} diff --git a/tests/Unit/ArrayDefinitionTest.php b/tests/Unit/ArrayDefinitionTest.php index 2964051..73a9e1f 100644 --- a/tests/Unit/ArrayDefinitionTest.php +++ b/tests/Unit/ArrayDefinitionTest.php @@ -15,6 +15,7 @@ use Yiisoft\Definitions\Tests\Support\EngineInterface; use Yiisoft\Definitions\Tests\Support\EngineMarkOne; use Yiisoft\Definitions\Tests\Support\EngineMarkTwo; +use Yiisoft\Definitions\Tests\Support\Mouse; use Yiisoft\Definitions\Tests\Support\Phone; use Yiisoft\Test\Support\Container\SimpleContainer; @@ -245,6 +246,181 @@ public function testCallFluentMethod(): void self::assertSame($country, $phone->getCountry()); } + public function dataMethodAutowiring(): array + { + return [ + [ + 'kitty', + EngineMarkOne::class, + ['kitty'], + ], + [ + 'kitty', + EngineMarkOne::class, + ['name' => 'kitty'], + ], + [ + 'kitty', + EngineMarkTwo::class, + ['kitty', new EngineMarkTwo()], + ], + [ + 'kitty', + EngineMarkTwo::class, + ['name' => 'kitty', 'engine' => new EngineMarkTwo()], + ], + [ + 'kitty', + EngineMarkTwo::class, + ['kitty', Reference::to('mark2')], + ], + ]; + } + + /** + * @dataProvider dataMethodAutowiring + */ + public function testMethodAutowiring(?string $expectedName, ?string $expectedEngine, array $data): void + { + $container = new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + 'mark2' => new EngineMarkTwo(), + ]); + + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Mouse::class, + 'setNameAndEngine()' => $data, + ]); + + /** @var Mouse $mouse */ + $mouse = $definition->resolve($container); + + self::assertSame($expectedName, $mouse->getName()); + self::assertInstanceOf($expectedEngine, $mouse->getEngine()); + } + + public function dataMethodVariadic(): array + { + return [ + [ + 'kitty', + [], + ['kitty'], + ], + [ + 'kitty', + [], + ['name' => 'kitty'], + ], + [ + 'kitty', + [], + ['name' => 'kitty', 'colors' => []], + ], + [ + 'kitty', + [1, 2, 3], + ['name' => 'kitty', 'colors' => [1, 2, 3]], + ], + [ + 'kitty', + [1, 2, 3], + ['kitty', 1, 2, 3], + ], + [ + 'kitty', + [[1, 2, 3]], + ['kitty', [1, 2, 3]], + ], + [ + 'kitty', + [1, 2, 3], + ['name' => 'kitty', 'colors' => Reference::to('data')], + ['data' => [1, 2, 3]], + ], + [ + 'kitty', + [[1, 2, 3]], + ['kitty', Reference::to('data')], + ['data' => [1, 2, 3]], + ], + ]; + } + + /** + * @dataProvider dataMethodVariadic + */ + public function testMethodVariadic( + ?string $expectedName, + array $expectedColors, + array $data, + array $containerDefinitions = [] + ): void { + $container = new SimpleContainer($containerDefinitions); + + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Mouse::class, + 'setNameAndColors()' => $data, + ]); + + /** @var Mouse $mouse */ + $mouse = $definition->resolve($container); + + self::assertSame($expectedName, $mouse->getName()); + self::assertSame($expectedColors, $mouse->getColors()); + } + + public function testArgumentsIndexedBothByNameAndByPositionInMethod(): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Mouse::class, + 'setNameAndEngine()' => ['kitty', 'engine' => new EngineMarkOne()], + ]); + $container = new SimpleContainer(); + + $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage( + 'Arguments indexed both by name and by position are not allowed in the same array.' + ); + $definition->resolve($container); + } + + public function testMethodWithWrongVariadicArgument(): void + { + $container = new SimpleContainer(); + + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Mouse::class, + 'setNameAndColors()' => [ + 'name' => 'kitty', + 'colors' => 'red', + ], + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Named argument for a variadic parameter should be an array, "string" given.'); + $definition->resolve($container); + } + + public function testMethodWithWrongReferenceVariadicArgument(): void + { + $container = new SimpleContainer([ + 'data' => 32, + ]); + + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Mouse::class, + 'setNameAndColors()' => [ + 'name' => 'kitty', + 'colors' => Reference::to('data'), + ], + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Named argument for a variadic parameter should be an array, "integer" given.'); + $definition->resolve($container); + } + public function testMerge(): void { $a = ArrayDefinition::fromConfig([