From e1546590f82dfcdd08f3fb0b7f35ac21bbd3b91f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 14 Mar 2023 16:56:43 +0100 Subject: [PATCH] [DependencyInjection] Generalize and simplify parsing of autowiring attributes --- .../Compiler/AutowirePass.php | 57 ++++++++----------- .../Tests/Compiler/AutowirePassTest.php | 7 ++- .../RegisterServiceSubscribersPassTest.php | 6 +- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 82bb84065247c..6cb09ccdfe92f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -25,7 +25,6 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\VarExporter\ProxyHelper; -use Symfony\Contracts\Service\Attribute\SubscribedService; /** * Inspects existing service definitions and wires the autowired ones using the type hints of their classes. @@ -106,18 +105,19 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed private function doProcessValue(mixed $value, bool $isRoot = false): mixed { if ($value instanceof TypedReference) { - if ($attributes = $value->getAttributes()) { - $attribute = array_pop($attributes); - - if ($attributes) { - throw new AutowiringFailedException($this->currentId, sprintf('Using multiple attributes with "%s" is not supported.', SubscribedService::class)); + foreach ($value->getAttributes() as $attribute) { + if ($attribute === $v = $this->processValue($attribute)) { + continue; } - - if (!$attribute instanceof Target) { - return $this->processAttribute($attribute, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $value->getInvalidBehavior()); + if (!$attribute instanceof Autowire || !$v instanceof Reference) { + return $v; } - $value = new TypedReference($value->getType(), $value->getType(), $value->getInvalidBehavior(), $attribute->name, [$attribute]); + $invalidBehavior = ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE !== $v->getInvalidBehavior() ? $v->getInvalidBehavior() : $value->getInvalidBehavior(); + $value = $v instanceof TypedReference + ? new TypedReference($v, $v->getType(), $invalidBehavior, $v->getName() ?? $value->getName(), array_merge($v->getAttributes(), $value->getAttributes())) + : new TypedReference($v, $value->getType(), $invalidBehavior, $value->getName(), $value->getAttributes()); + break; } if ($ref = $this->getAutowiredReference($value, true)) { return $ref; @@ -173,22 +173,6 @@ private function doProcessValue(mixed $value, bool $isRoot = false): mixed return $value; } - private function processAttribute(object $attribute, bool $isOptional = false): mixed - { - switch (true) { - case $attribute instanceof Autowire: - if ($isOptional && $attribute->value instanceof Reference) { - return new Reference($attribute->value, ContainerInterface::NULL_ON_INVALID_REFERENCE); - } - // no break - case $attribute instanceof AutowireDecorated: - case $attribute instanceof MapDecorated: - return $this->processValue($attribute); - } - - throw new AutowiringFailedException($this->currentId, sprintf('"%s" is an unsupported attribute.', $attribute::class)); - } - private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array { $this->decoratedId = null; @@ -281,11 +265,15 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a } $type = ProxyHelper::exportType($parameter, true); + $target = null; + $name = Target::parseName($parameter, $target); + $target = $target ? [$target] : []; if ($checkAttributes) { foreach ($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { $attribute = $attribute->newInstance(); - $value = $this->processAttribute($attribute, $parameter->allowsNull()); + $invalidBehavior = $parameter->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE; + $value = $this->processValue(new TypedReference($type ?: '?', $type ?: 'mixed', $invalidBehavior, $name, [$attribute, ...$target])); if ($attribute instanceof AutowireCallable || 'Closure' === $type && \is_array($value)) { $value = (new Definition('Closure')) @@ -299,13 +287,13 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a } foreach ($parameter->getAttributes(AutowireDecorated::class) as $attribute) { - $arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull()); + $arguments[$index] = $this->processValue($attribute->newInstance()); continue 2; } foreach ($parameter->getAttributes(MapDecorated::class) as $attribute) { - $arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull()); + $arguments[$index] = $this->processValue($attribute->newInstance()); continue 2; } @@ -338,9 +326,8 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a continue; } - $getValue = function () use ($type, $parameter, $class, $method) { - $name = Target::parseName($parameter, $target); - if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $name, $target ? [$target] : []), false)) { + $getValue = function () use ($type, $parameter, $class, $method, $name, $target) { + if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $name, $target), false)) { $failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); if ($parameter->isDefaultValueAvailable()) { @@ -354,7 +341,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a return $value; }; - if ($this->decoratedClass && $isDecorated = is_a($this->decoratedClass, $type, true)) { + if ($this->decoratedClass && is_a($this->decoratedClass, $type, true)) { if ($this->getPreviousValue) { // The inner service is injected only if there is only 1 argument matching the type of the decorated class // across all arguments of all autowired methods. @@ -412,7 +399,9 @@ private function getAutowiredReference(TypedReference $reference, bool $filterTy $type = implode($m[0], $types); } - if (null !== $name = $reference->getName()) { + $name = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name; + + if (null !== $name ??= $reference->getName()) { if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) { return new TypedReference($alias, $type, $reference->getInvalidBehavior()); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index bd51657278526..50ffdf7ae2d9a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Compiler\AutowireAsDecoratorPass; use Symfony\Component\DependencyInjection\Compiler\AutowirePass; use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; @@ -1302,16 +1303,16 @@ public function testAutowireAttribute() $definition = $container->getDefinition(AutowireAttribute::class); $this->assertCount(10, $definition->getArguments()); - $this->assertEquals(new Reference('some.id'), $definition->getArgument(0)); + $this->assertEquals(new TypedReference('some.id', 'stdClass', attributes: [new Autowire(service: 'some.id')]), $definition->getArgument(0)); $this->assertEquals(new Expression("parameter('some.parameter')"), $definition->getArgument(1)); $this->assertSame('foo/bar', $definition->getArgument(2)); $this->assertNull($definition->getArgument(3)); - $this->assertEquals(new Reference('some.id'), $definition->getArgument(4)); + $this->assertEquals(new TypedReference('some.id', 'stdClass', attributes: [new Autowire(service: 'some.id')]), $definition->getArgument(4)); $this->assertEquals(new Expression("parameter('some.parameter')"), $definition->getArgument(5)); $this->assertSame('bar', $definition->getArgument(6)); $this->assertSame('@bar', $definition->getArgument(7)); $this->assertSame('foo', $definition->getArgument(8)); - $this->assertEquals(new Reference('invalid.id', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(9)); + $this->assertEquals(new TypedReference('invalid.id', 'stdClass', ContainerInterface::NULL_ON_INVALID_REFERENCE, attributes: [new Autowire(service: 'invalid.id')]), $definition->getArgument(9)); $container->compile(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index ffda5af3e2dd8..e671bef672dd6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -459,11 +459,11 @@ public static function getSubscribedServices(): array $expected = [ 'tagged.iterator' => new ServiceClosureArgument(new TaggedIteratorArgument('tag')), 'tagged.locator' => new ServiceClosureArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('tag', 'tag', needsIndexes: true))), - 'autowired' => new ServiceClosureArgument(new Reference('service.id')), - 'autowired.nullable' => new ServiceClosureArgument(new Reference('service.id', ContainerInterface::NULL_ON_INVALID_REFERENCE)), + 'autowired' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired', [new Autowire(service: 'service.id')])), + 'autowired.nullable' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'autowired.nullable', [new Autowire(service: 'service.id')])), 'autowired.parameter' => new ServiceClosureArgument('foobar'), 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.QVDPERh.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), - 'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'someTarget', [new Target('someTarget')])), + 'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'target', [new Target('someTarget')])), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); }