diff --git a/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php b/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php new file mode 100644 index 0000000000000..81680abaa1849 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class TaggedIterator +{ + public function __construct( + public string $tag, + public ?string $indexAttribute = null, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php b/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php new file mode 100644 index 0000000000000..3c1d8037093a0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class TaggedLocator +{ + public function __construct( + public string $tag, + public ?string $indexAttribute = null, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index aa1ff13314352..5a42dd348fd30 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8 * Add `#[AsTaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators * Add autoconfigurable attributes + * Add support for autowiring tagged iterators and locators via attributes on PHP 8 * Add support for per-env configuration in loaders * Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration * Add support an integer return value for default_index_method diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php index aefa7c0c6176b..c847341cce16f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php @@ -13,38 +13,44 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; /** * @author Alexander M. Turek */ -final class AttributeAutoconfigurationPass implements CompilerPassInterface +final class AttributeAutoconfigurationPass extends AbstractRecursivePass { public function process(ContainerBuilder $container): void { - if (80000 > \PHP_VERSION_ID) { + if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) { return; } - $autoconfiguredAttributes = $container->getAutoconfiguredAttributes(); + parent::process($container); + } - foreach ($container->getDefinitions() as $id => $definition) { - if (!$definition->isAutoconfigured() - || $definition->isAbstract() - || $definition->hasTag('container.ignore_attributes') - || !($reflector = $container->getReflectionClass($definition->getClass(), false)) - ) { - continue; - } + protected function processValue($value, bool $isRoot = false) + { + if (!$value instanceof Definition + || !$value->isAutoconfigured() + || $value->isAbstract() + || $value->hasTag('container.ignore_attributes') + || !($reflector = $this->container->getReflectionClass($value->getClass(), false)) + ) { + return parent::processValue($value, $isRoot); + } - $instanceof = $definition->getInstanceofConditionals(); - $conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition(''); - foreach ($reflector->getAttributes() as $attribute) { - if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) { - $configurator($conditionals, $attribute->newInstance(), $reflector); - } + $autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes(); + $instanceof = $value->getInstanceofConditionals(); + $conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition(''); + foreach ($reflector->getAttributes() as $attribute) { + if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $reflector); } - $instanceof[$reflector->getName()] = $conditionals; - $definition->setInstanceofConditionals($instanceof); } + $instanceof[$reflector->getName()] = $conditionals; + $value->setInstanceofConditionals($instanceof); + + return parent::processValue($value, $isRoot); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index cb6e8bc4ed1b3..af6d6925d6e08 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -12,6 +12,10 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\Config\Resource\ClassExistenceResource; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; +use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; @@ -123,7 +127,8 @@ private function doProcessValue($value, bool $isRoot = false) array_unshift($this->methodCalls, [$constructor, $value->getArguments()]); } - $this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot); + $checkAttributes = 80000 <= \PHP_VERSION_ID && !$value->hasTag('container.ignore_attributes'); + $this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot, $checkAttributes); if ($constructor) { [, $arguments] = array_shift($this->methodCalls); @@ -140,7 +145,7 @@ private function doProcessValue($value, bool $isRoot = false) return $value; } - private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): array + private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array { $this->decoratedId = null; $this->decoratedClass = null; @@ -168,7 +173,7 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): } } - $arguments = $this->autowireMethod($reflectionMethod, $arguments); + $arguments = $this->autowireMethod($reflectionMethod, $arguments, $checkAttributes); if ($arguments !== $call[1]) { $this->methodCalls[$i][1] = $arguments; @@ -185,7 +190,7 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): * * @throws AutowiringFailedException */ - private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments): array + private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes): array { $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId; $method = $reflectionMethod->name; @@ -201,6 +206,26 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); + if ($checkAttributes) { + foreach ($parameter->getAttributes() as $attribute) { + if (TaggedIterator::class === $attribute->getName()) { + $attribute = $attribute->newInstance(); + $arguments[$index] = new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute); + break; + } + + if (TaggedLocator::class === $attribute->getName()) { + $attribute = $attribute->newInstance(); + $arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute)); + break; + } + } + + if ('' !== ($arguments[$index] ?? '')) { + continue; + } + } + if (!$type) { if (isset($arguments[$index])) { continue; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 15febf9f70fd1..12d3b26f740df 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -62,10 +62,10 @@ public function __construct() new AutowireRequiredMethodsPass(), new AutowireRequiredPropertiesPass(), new ResolveBindingsPass(), - new ServiceLocatorTagPass(), new DecoratorServicePass(), new CheckDefinitionValidityPass(), new AutowirePass(false), + new ServiceLocatorTagPass(), new ResolveTaggedIteratorArgumentPass(), new ResolveServiceSubscribersPass(), new ResolveReferencesToAliasesPass(), diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index aaaec478ac5a1..48f7c0186e2b4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -28,6 +28,10 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\IteratorConsumer; +use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumer; +use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerConsumer; +use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerFactory; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3; @@ -317,6 +321,33 @@ public function testTaggedServiceWithIndexAttributeAndDefaultMethod() $this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param); } + /** + * @requires PHP 8 + */ + public function testTaggedServiceWithIndexAttributeAndDefaultMethodConfiguredViaAttribute() + { + $container = new ContainerBuilder(); + $container->register(BarTagClass::class) + ->setPublic(true) + ->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod']) + ; + $container->register(FooTagClass::class) + ->setPublic(true) + ->addTag('foo_bar', ['foo' => 'foo']) + ; + $container->register(IteratorConsumer::class) + ->setAutowired(true) + ->setPublic(true) + ; + + $container->compile(); + + $s = $container->get(IteratorConsumer::class); + + $param = iterator_to_array($s->getParam()->getIterator()); + $this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param); + } + public function testTaggedIteratorWithMultipleIndexAttribute() { $container = new ContainerBuilder(); @@ -343,6 +374,88 @@ public function testTaggedIteratorWithMultipleIndexAttribute() $this->assertSame(['bar' => $container->get(BarTagClass::class), 'bar_duplicate' => $container->get(BarTagClass::class), 'foo_tag_class' => $container->get(FooTagClass::class)], $param); } + /** + * @requires PHP 8 + */ + public function testTaggedLocatorConfiguredViaAttribute() + { + $container = new ContainerBuilder(); + $container->register(BarTagClass::class) + ->setPublic(true) + ->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod']) + ; + $container->register(FooTagClass::class) + ->setPublic(true) + ->addTag('foo_bar', ['foo' => 'foo']) + ; + $container->register(LocatorConsumer::class) + ->setAutowired(true) + ->setPublic(true) + ; + + $container->compile(); + + /** @var LocatorConsumer $s */ + $s = $container->get(LocatorConsumer::class); + + $locator = $s->getLocator(); + self::assertSame($container->get(BarTagClass::class), $locator->get('bar_tab_class_with_defaultmethod')); + self::assertSame($container->get(FooTagClass::class), $locator->get('foo')); + } + + /** + * @requires PHP 8 + */ + public function testNestedDefinitionWithAutoconfiguredConstructorArgument() + { + $container = new ContainerBuilder(); + $container->register(FooTagClass::class) + ->setPublic(true) + ->addTag('foo_bar', ['foo' => 'foo']) + ; + $container->register(LocatorConsumerConsumer::class) + ->setPublic(true) + ->setArguments([ + (new Definition(LocatorConsumer::class)) + ->setAutowired(true), + ]) + ; + + $container->compile(); + + /** @var LocatorConsumerConsumer $s */ + $s = $container->get(LocatorConsumerConsumer::class); + + $locator = $s->getLocatorConsumer()->getLocator(); + self::assertSame($container->get(FooTagClass::class), $locator->get('foo')); + } + + /** + * @requires PHP 8 + */ + public function testFactoryWithAutoconfiguredArgument() + { + $container = new ContainerBuilder(); + $container->register(FooTagClass::class) + ->setPublic(true) + ->addTag('foo_bar', ['key' => 'my_service']) + ; + $container->register(LocatorConsumerFactory::class); + $container->register(LocatorConsumer::class) + ->setPublic(true) + ->setAutowired(true) + ->setFactory(new Reference(LocatorConsumerFactory::class)) + ; + + $container->compile(); + + /** @var LocatorConsumer $s */ + $s = $container->get(LocatorConsumer::class); + + $locator = $s->getLocator(); + self::assertSame($container->get(FooTagClass::class), $locator->get('my_service')); + } + public function testTaggedServiceWithDefaultPriorityMethod() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/IteratorConsumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/IteratorConsumer.php new file mode 100644 index 0000000000000..329a14f39331d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/IteratorConsumer.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + +final class IteratorConsumer +{ + public function __construct( + #[TaggedIterator('foo_bar', indexAttribute: 'foo')] + private iterable $param, + ) { + } + + public function getParam(): iterable + { + return $this->param; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumer.php new file mode 100644 index 0000000000000..487cce16c0da8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumer.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; + +final class LocatorConsumer +{ + public function __construct( + #[TaggedLocator('foo_bar', indexAttribute: 'foo')] + private ContainerInterface $locator, + ) { + } + + public function getLocator(): ContainerInterface + { + return $this->locator; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumerConsumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumerConsumer.php new file mode 100644 index 0000000000000..c686754c5ad7e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumerConsumer.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +final class LocatorConsumerConsumer +{ + public function __construct( + private LocatorConsumer $locatorConsumer + ) { + } + + public function getLocatorConsumer(): LocatorConsumer + { + return $this->locatorConsumer; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumerFactory.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumerFactory.php new file mode 100644 index 0000000000000..4783e0cb609a2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/LocatorConsumerFactory.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; + +final class LocatorConsumerFactory +{ + public function __invoke( + #[TaggedLocator('foo_bar', indexAttribute: 'key')] + ContainerInterface $locator + ): LocatorConsumer { + return new LocatorConsumer($locator); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/MultipleArgumentBindings.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/MultipleArgumentBindings.php new file mode 100644 index 0000000000000..4442a6bc08c74 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/MultipleArgumentBindings.php @@ -0,0 +1,15 @@ + true, '.service_locator.DlIAmAe.foo_service' => true, + '.service_locator.t5IGRMW' => true, 'Psr\\Container\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,