From 252f2ca1fb2fd8b79aa9c8ee37495a5b074c131c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 18 Feb 2021 22:51:00 +0100 Subject: [PATCH] [DependencyInjection] Add `#[TaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators --- .../Attribute/TaggedItem.php | 27 ++++++++ .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/PriorityTaggedServiceTrait.php | 69 +++++++++---------- .../PriorityTaggedServiceTraitTest.php | 36 ++++++++++ 4 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/TaggedItem.php diff --git a/src/Symfony/Component/DependencyInjection/Attribute/TaggedItem.php b/src/Symfony/Component/DependencyInjection/Attribute/TaggedItem.php new file mode 100644 index 0000000000000..b7ba825ae8e5f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/TaggedItem.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +/** + * An attribute to tell under which index and priority a service class should be found in tagged iterators/locators. + * + * @author Nicolas Grekas + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class TaggedItem +{ + public function __construct( + public string $index, + public ?int $priority = null, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 82837aa0e5e96..26bc5690a22fb 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `ServicesConfigurator::remove()` in the PHP-DSL * Add `%env(not:...)%` processor to negate boolean values * Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8 + * Add `#[TaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators * Add autoconfigurable attributes * Add support for per-env configuration in loaders * Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php index 30b31eed16287..3328274dc991e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Attribute\TaggedItem; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; @@ -56,8 +57,10 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) { $defaultPriority = null; $defaultIndex = null; - $class = $container->getDefinition($serviceId)->getClass(); + $definition = $container->getDefinition($serviceId); + $class = $definition->getClass(); $class = $container->getParameterBag()->resolveValue($class) ?: null; + $checkTaggedItem = !$definition->hasTag(80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() ? 'container.ignore_attributes' : $tagName); foreach ($attributes as $attribute) { $index = $priority = null; @@ -65,7 +68,7 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container if (isset($attribute['priority'])) { $priority = $attribute['priority']; } elseif (null === $defaultPriority && $defaultPriorityMethod && $class) { - $defaultPriority = PriorityTaggedServiceUtil::getDefaultPriority($container, $serviceId, $class, $defaultPriorityMethod, $tagName); + $defaultPriority = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem); } $priority = $priority ?? $defaultPriority ?? $defaultPriority = 0; @@ -77,7 +80,7 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container if (null !== $indexAttribute && isset($attribute[$indexAttribute])) { $index = $attribute[$indexAttribute]; } elseif (null === $defaultIndex && $defaultPriorityMethod && $class) { - $defaultIndex = PriorityTaggedServiceUtil::getDefaultIndex($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute); + $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem); } $index = $index ?? $defaultIndex ?? $defaultIndex = $serviceId; @@ -114,22 +117,30 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container class PriorityTaggedServiceUtil { /** - * Gets the index defined by the default index method. + * @return string|int|null */ - public static function getDefaultIndex(ContainerBuilder $container, string $serviceId, string $class, string $defaultIndexMethod, string $tagName, ?string $indexAttribute): ?string + public static function getDefault(ContainerBuilder $container, string $serviceId, string $class, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem) { - if (!($r = $container->getReflectionClass($class)) || !$r->hasMethod($defaultIndexMethod)) { + if (!($r = $container->getReflectionClass($class)) || (!$checkTaggedItem && !$r->hasMethod($defaultMethod))) { + return null; + } + + if ($checkTaggedItem && !$r->hasMethod($defaultMethod)) { + foreach ($r->getAttributes(TaggedItem::class) as $attribute) { + return 'priority' === $indexAttribute ? $attribute->newInstance()->priority : $attribute->newInstance()->index; + } + return null; } if (null !== $indexAttribute) { $service = $class !== $serviceId ? sprintf('service "%s"', $serviceId) : 'on the corresponding service'; - $message = [sprintf('Either method "%s::%s()" should ', $class, $defaultIndexMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)]; + $message = [sprintf('Either method "%s::%s()" should ', $class, $defaultMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)]; } else { - $message = [sprintf('Method "%s::%s()" should ', $class, $defaultIndexMethod), '.']; + $message = [sprintf('Method "%s::%s()" should ', $class, $defaultMethod), '.']; } - if (!($rm = $r->getMethod($defaultIndexMethod))->isStatic()) { + if (!($rm = $r->getMethod($defaultMethod))->isStatic()) { throw new InvalidArgumentException(implode('be static', $message)); } @@ -137,42 +148,24 @@ public static function getDefaultIndex(ContainerBuilder $container, string $serv throw new InvalidArgumentException(implode('be public', $message)); } - $defaultIndex = $rm->invoke(null); + $default = $rm->invoke(null); - if (\is_int($defaultIndex)) { - $defaultIndex = (string) $defaultIndex; - } - - if (!\is_string($defaultIndex)) { - throw new InvalidArgumentException(implode(sprintf('return string|int (got "%s")', get_debug_type($defaultIndex)), $message)); - } - - return $defaultIndex; - } - - /** - * Gets the priority defined by the default priority method. - */ - public static function getDefaultPriority(ContainerBuilder $container, string $serviceId, string $class, string $defaultPriorityMethod, string $tagName): ?int - { - if (!($r = $container->getReflectionClass($class)) || !$r->hasMethod($defaultPriorityMethod)) { - return null; - } + if ('priority' === $indexAttribute) { + if (!\is_int($default)) { + throw new InvalidArgumentException(implode(sprintf('return int (got "%s")', get_debug_type($default)), $message)); + } - if (!($rm = $r->getMethod($defaultPriorityMethod))->isStatic()) { - throw new InvalidArgumentException(sprintf('Either method "%s::%s()" should be static or tag "%s" on service "%s" is missing attribute "priority".', $class, $defaultPriorityMethod, $tagName, $serviceId)); + return $default; } - if (!$rm->isPublic()) { - throw new InvalidArgumentException(sprintf('Either method "%s::%s()" should be public or tag "%s" on service "%s" is missing attribute "priority".', $class, $defaultPriorityMethod, $tagName, $serviceId)); + if (\is_int($default)) { + $default = (string) $default; } - $defaultPriority = $rm->invoke(null); - - if (!\is_int($defaultPriority)) { - throw new InvalidArgumentException(sprintf('Method "%s::%s()" should return an integer (got "%s") or tag "%s" on service "%s" is missing attribute "priority".', $class, $defaultPriorityMethod, get_debug_type($defaultPriority), $tagName, $serviceId)); + if (!\is_string($default)) { + throw new InvalidArgumentException(implode(sprintf('return string|int (got "%s")', get_debug_type($default)), $message)); } - return $defaultPriority; + return $default; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php index 5c150a26d0eb8..1ef1521b7d86c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php @@ -13,7 +13,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Attribute\TaggedItem; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; @@ -188,6 +191,34 @@ public function provideInvalidDefaultMethods(): iterable yield ['getMethodShouldBePublicInsteadPrivate', null, sprintf('Method "%s::getMethodShouldBePublicInsteadPrivate()" should be public.', FooTaggedForInvalidDefaultMethodClass::class)]; yield ['getMethodShouldBePublicInsteadPrivate', 'foo', sprintf('Either method "%s::getMethodShouldBePublicInsteadPrivate()" should be public or tag "my_custom_tag" on service "service1" is missing attribute "foo".', FooTaggedForInvalidDefaultMethodClass::class)]; } + + /** + * @requires PHP 8 + */ + public function testTaggedItemAttributes() + { + $container = new ContainerBuilder(); + $container->register('service1', FooTagClass::class)->addTag('my_custom_tag'); + $container->register('service2', HelloNamedService::class) + ->setAutoconfigured(true) + ->setInstanceofConditionals([ + HelloNamedService::class => (new ChildDefinition(''))->addTag('my_custom_tag'), + \stdClass::class => (new ChildDefinition(''))->addTag('my_custom_tag2'), + ]); + + (new ResolveInstanceofConditionalsPass())->process($container); + + $priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation(); + + $tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar'); + $expected = [ + 'hello' => new TypedReference('service2', HelloNamedService::class), + 'service1' => new TypedReference('service1', FooTagClass::class), + ]; + $services = $priorityTaggedServiceTraitImplementation->test($tag, $container); + $this->assertSame(array_keys($expected), array_keys($services)); + $this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container)); + } } class PriorityTaggedServiceTraitImplementation @@ -199,3 +230,8 @@ public function test($tagName, ContainerBuilder $container) return $this->findAndSortTaggedServices($tagName, $container); } } + +#[TaggedItem(index: 'hello', priority: 1)] +class HelloNamedService extends \stdClass +{ +}