From aba31f94e7ed5b07aa88b161b64871c4e372d393 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 13 Oct 2021 18:39:52 +0200 Subject: [PATCH] [DependencyInjection] autowire union and intersection types --- .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/AutowirePass.php | 57 ++++++++++++++++++- .../LazyProxy/ProxyHelper.php | 2 + .../Tests/Compiler/AutowirePassTest.php | 48 +++++++++++++++- 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 46cd7f69ded0e..0abbb366f5ef8 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `service_closure()` to the PHP-DSL * Add support for autoconfigurable attributes on methods, properties and parameters * Make auto-aliases private by default + * Add support for autowiring union and intersection types 5.3 --- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 45cb642422349..fbbc83c37780b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -45,6 +45,7 @@ class AutowirePass extends AbstractRecursivePass private $decoratedMethodIndex; private $decoratedMethodArgumentIndex; private $typesClone; + private $combinedAliases; public function __construct(bool $throwOnAutowireException = true) { @@ -60,6 +61,8 @@ public function __construct(bool $throwOnAutowireException = true) */ public function process(ContainerBuilder $container) { + $this->populateCombinedAliases($container); + try { $this->typesClone = clone $this; parent::process($container); @@ -72,6 +75,7 @@ public function process(ContainerBuilder $container) $this->decoratedMethodIndex = null; $this->decoratedMethodArgumentIndex = null; $this->typesClone = null; + $this->combinedAliases = []; } } @@ -223,8 +227,6 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, /** * Autowires the constructor or a method. * - * @return array - * * @throws AutowiringFailedException */ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes, int $methodIndex): array @@ -363,8 +365,12 @@ private function getAutowiredReference(TypedReference $reference): ?TypedReferen return new TypedReference($alias, $type, $reference->getInvalidBehavior()); } + if (null !== ($alias = $this->combinedAliases[$alias] ?? null) && !$this->container->findDefinition($alias)->isAbstract()) { + return new TypedReference($alias, $type, $reference->getInvalidBehavior()); + } + if ($this->container->has($name) && !$this->container->findDefinition($name)->isAbstract()) { - foreach ($this->container->getAliases() as $id => $alias) { + foreach ($this->container->getAliases() + $this->combinedAliases as $id => $alias) { if ($name === (string) $alias && str_starts_with($id, $type.' $')) { return new TypedReference($name, $type, $reference->getInvalidBehavior()); } @@ -376,6 +382,10 @@ private function getAutowiredReference(TypedReference $reference): ?TypedReferen return new TypedReference($type, $type, $reference->getInvalidBehavior()); } + if (null !== ($alias = $this->combinedAliases[$type] ?? null) && !$this->container->findDefinition($alias)->isAbstract()) { + return new TypedReference($alias, $type, $reference->getInvalidBehavior()); + } + return null; } @@ -565,4 +575,45 @@ private function populateAutowiringAlias(string $id): void $this->autowiringAliases[$type][$name] = $name; } } + + private function populateCombinedAliases(ContainerBuilder $container): void + { + $this->combinedAliases = []; + $reverseAliases = []; + + foreach ($container->getAliases() as $id => $alias) { + if (!preg_match('/(?(DEFINE)(?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) { + continue; + } + + $type = $m[2]; + $name = $m[3] ?? ''; + $reverseAliases[(string) $alias][$name][] = $type; + } + + foreach ($reverseAliases as $alias => $names) { + foreach ($names as $name => $types) { + if (2 > $count = \count($types)) { + continue; + } + sort($types); + $i = 1 << $count; + + // compute the powerset of the list of types + while ($i--) { + $set = []; + for ($j = 0; $j < $count; ++$j) { + if ($i & (1 << $j)) { + $set[] = $types[$j]; + } + } + + if (2 <= \count($set)) { + $this->combinedAliases[implode('&', $set).('' === $name ? '' : ' $'.$name)] = $alias; + $this->combinedAliases[implode('|', $set).('' === $name ? '' : ' $'.$name)] = $alias; + } + } + } + } + } } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php index 32b94df04bd95..8eb45b548bbb7 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php @@ -70,6 +70,8 @@ public static function getTypeHint(\ReflectionFunctionAbstract $r, \ReflectionPa } } + sort($types); + return $types ? implode($glue, $types) : null; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 33e9adecfb4ea..14a31b7f0e8b4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -258,13 +258,31 @@ public function testTypeNotGuessableUnionType() $pass->process($container); } + /** + * @requires PHP 8 + */ + public function testGuessableUnionType() + { + $container = new ContainerBuilder(); + + $container->register('b', \stcClass::class); + $container->setAlias(CollisionA::class.' $collision', 'b'); + $container->setAlias(CollisionB::class.' $collision', 'b'); + + $aDefinition = $container->register('a', UnionClasses::class); + $aDefinition->setAutowired(true); + + $pass = new AutowirePass(); + $pass->process($container); + + $this->assertSame('b', (string) $aDefinition->getArgument(0)); + } + /** * @requires PHP 8.1 */ public function testTypeNotGuessableIntersectionType() { - $this->expectException(AutowiringFailedException::class); - $this->expectExceptionMessage('Cannot autowire service "a": argument "$collision" of method "Symfony\Component\DependencyInjection\Tests\Compiler\IntersectionClasses::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface&Symfony\Component\DependencyInjection\Tests\Compiler\AnotherInterface" but this class was not found.'); $container = new ContainerBuilder(); $container->register(CollisionInterface::class); @@ -273,8 +291,32 @@ public function testTypeNotGuessableIntersectionType() $aDefinition = $container->register('a', IntersectionClasses::class); $aDefinition->setAutowired(true); + $pass = new AutowirePass(); + + $this->expectException(AutowiringFailedException::class); + $this->expectExceptionMessage('Cannot autowire service "a": argument "$collision" of method "Symfony\Component\DependencyInjection\Tests\Compiler\IntersectionClasses::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\AnotherInterface&Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" but this class was not found.'); + $pass->process($container); + } + + /** + * @requires PHP 8.1 + */ + public function testGuessableIntersectionType() + { + $container = new ContainerBuilder(); + + $container->register('b', \stcClass::class); + $container->setAlias(CollisionInterface::class, 'b'); + $container->setAlias(AnotherInterface::class, 'b'); + $container->setAlias(DummyInterface::class, 'b'); + + $aDefinition = $container->register('a', IntersectionClasses::class); + $aDefinition->setAutowired(true); + $pass = new AutowirePass(); $pass->process($container); + + $this->assertSame('b', (string) $aDefinition->getArgument(0)); } public function testTypeNotGuessableWithTypeSet() @@ -516,7 +558,7 @@ public function testScalarArgsCannotBeAutowired() public function testUnionScalarArgsCannotBeAutowired() { $this->expectException(AutowiringFailedException::class); - $this->expectExceptionMessage('Cannot autowire service "union_scalars": argument "$timeout" of method "Symfony\Component\DependencyInjection\Tests\Compiler\UnionScalars::__construct()" is type-hinted "int|float", you should configure its value explicitly.'); + $this->expectExceptionMessage('Cannot autowire service "union_scalars": argument "$timeout" of method "Symfony\Component\DependencyInjection\Tests\Compiler\UnionScalars::__construct()" is type-hinted "float|int", you should configure its value explicitly.'); $container = new ContainerBuilder(); $container->register('union_scalars', UnionScalars::class)