diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php index de751213acad5..3b94c60979db1 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php @@ -20,8 +20,8 @@ class AsTaggedItem { /** - * @param string|null $index The property or method to use to index the item in the iterator/locator - * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the iterator/locator + * @param string|null $index The index at which the service will be found when consuming tagged iterators/locators + * @param int|null $priority The priority of the service in iterators/locators; the higher the number, the earlier it will */ public function __construct( public ?string $index = null, diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php index 8c6b5b582770d..99a4556e5e403 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php @@ -65,47 +65,57 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam $class = $definition->getClass(); $class = $container->getParameterBag()->resolveValue($class) ?: null; $reflector = null !== $class ? $container->getReflectionClass($class) : null; - $checkTaggedItem = !$definition->hasTag($definition->isAutoconfigured() ? 'container.ignore_attributes' : $tagName); + $loadFromDefaultMethods = $reflector && null !== $defaultPriorityMethod; + $phpAttributes = $definition->isAutoconfigured() && !$definition->hasTag('container.ignore_attributes') ? $reflector?->getAttributes(AsTaggedItem::class) : []; + + foreach ($phpAttributes ??= [] as $i => $attribute) { + $attribute = $attribute->newInstance(); + $phpAttributes[$i] = [ + 'priority' => $attribute->priority, + $indexAttribute ?? '' => $attribute->index, + ]; + if (null === $defaultPriority) { + $defaultPriority = $attribute->priority ?? 0; + $defaultIndex = $attribute->index; + } + } + if (1 >= \count($phpAttributes)) { + $phpAttributes = []; + } + + for ($i = 0; $i < \count($attributes); ++$i) { + if (!($attribute = $attributes[$i]) && $phpAttributes) { + array_splice($attributes, $i--, 1, $phpAttributes); + continue; + } - foreach ($attributes as $attribute) { $index = $priority = null; if (isset($attribute['priority'])) { $priority = $attribute['priority']; - } elseif (null === $defaultPriority && $defaultPriorityMethod && $reflector) { - $defaultPriority = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem); + } elseif ($loadFromDefaultMethods) { + $defaultPriority = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultPriorityMethod, $tagName, 'priority') ?? $defaultPriority; + $defaultIndex = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute) ?? $defaultIndex; + $loadFromDefaultMethods = false; } $priority ??= $defaultPriority ??= 0; if (null === $indexAttribute && !$defaultIndexMethod && !$needsIndexes) { - $services[] = [$priority, ++$i, null, $serviceId, null]; + $services[] = [$priority, $i, null, $serviceId, null]; continue 2; } if (null !== $indexAttribute && isset($attribute[$indexAttribute])) { $index = $parameterBag->resolveValue($attribute[$indexAttribute]); } - if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $reflector) { - $defaultIndex = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem); + if (null === $index && $loadFromDefaultMethods) { + $defaultPriority = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultPriorityMethod, $tagName, 'priority') ?? $defaultPriority; + $defaultIndex = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute) ?? $defaultIndex; + $loadFromDefaultMethods = false; } $index ??= $defaultIndex ??= $definition->getTag('container.decorator')[0]['id'] ?? $serviceId; - $services[] = [$priority, ++$i, $index, $serviceId, $class]; - } - - if ($reflector) { - $attributes = $reflector->getAttributes(AsTaggedItem::class); - $attributeCount = \count($attributes); - - foreach ($attributes as $attribute) { - $instance = $attribute->newInstance(); - - if (!$instance->index && 1 < $attributeCount) { - throw new InvalidArgumentException(\sprintf('Attribute "%s" on class "%s" cannot have an empty index when repeated.', AsTaggedItem::class, $class)); - } - - $services[] = [$instance->priority ?? 0, ++$i, $instance->index ?? $serviceId, $serviceId, $class]; - } + $services[] = [$priority, $i, $index, $serviceId, $class]; } } @@ -113,13 +123,11 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam $refs = []; foreach ($services as [, , $index, $serviceId, $class]) { - if (!$class) { - $reference = new Reference($serviceId); - } elseif ($index === $serviceId) { - $reference = new TypedReference($serviceId, $class); - } else { - $reference = new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $index); - } + $reference = match (true) { + !$class => new Reference($serviceId), + $index === $serviceId => new TypedReference($serviceId, $class), + default => new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $index), + }; if (null === $index) { $refs[] = $reference; @@ -137,25 +145,16 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam */ class PriorityTaggedServiceUtil { - public static function getDefault(string $serviceId, \ReflectionClass $r, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem): string|int|null + public static function getDefault(string $serviceId, \ReflectionClass $r, string $defaultMethod, string $tagName, ?string $indexAttribute): string|int|null { - $class = $r->getName(); - - if (!$checkTaggedItem && !$r->hasMethod($defaultMethod)) { - return null; - } - - if ($checkTaggedItem && !$r->hasMethod($defaultMethod)) { - foreach ($r->getAttributes(AsTaggedItem::class) as $attribute) { - return 'priority' === $indexAttribute ? $attribute->newInstance()->priority : $attribute->newInstance()->index; - } - + if (!$r->hasMethod($defaultMethod)) { return null; } if ($r->isInterface()) { return null; } + $class = $r->name; if (null !== $indexAttribute) { $service = $class !== $serviceId ? \sprintf('service "%s"', $serviceId) : 'on the corresponding service'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php index 3f767257def91..aa67cf96cf236 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php @@ -233,6 +233,7 @@ public function testTaggedItemAttributes() 'hello' => new TypedReference('service2', HelloNamedService::class), 'multi_hello_1' => new TypedReference('service6', MultiTagHelloNamedService::class), 'service1' => new TypedReference('service1', FooTagClass::class), + 'multi_hello_0' => new TypedReference('service6', MultiTagHelloNamedService::class), ]; $services = $priorityTaggedServiceTraitImplementation->test($tag, $container); @@ -240,22 +241,6 @@ public function testTaggedItemAttributes() $this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container)); } - public function testTaggedItemAttributesRepeatedWithoutNameThrows() - { - $container = new ContainerBuilder(); - $container->register('service1', MultiNoNameTagHelloNamedService::class) - ->setAutoconfigured(true) - ->addTag('my_custom_tag'); - - (new ResolveInstanceofConditionalsPass())->process($container); - $tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar', exclude: ['service4', 'service5']); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Attribute "Symfony\Component\DependencyInjection\Attribute\AsTaggedItem" on class "Symfony\Component\DependencyInjection\Tests\Compiler\MultiNoNameTagHelloNamedService" cannot have an empty index when repeated.'); - - (new PriorityTaggedServiceTraitImplementation())->test($tag, $container); - } - public function testResolveIndexedTags() { $container = new ContainerBuilder(); @@ -283,6 +268,48 @@ public function testResolveIndexedTags() $this->assertSame(array_keys($expected), array_keys($services)); $this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container)); } + + public function testAttributesAreMergedWithTags() + { + $container = new ContainerBuilder(); + $definition = $container->register('service_attr_first', MultiTagHelloNamedService::class); + $definition->setAutoconfigured(true); + $definition->addTag('my_custom_tag', ['foo' => 'z']); + $definition->addTag('my_custom_tag', []); + + (new ResolveInstanceofConditionalsPass())->process($container); + + $priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation(); + + $tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar'); + $services = $priorityTaggedServiceTraitImplementation->test($tag, $container); + + $expected = [ + 'multi_hello_2' => new TypedReference('service_attr_first', MultiTagHelloNamedService::class), + 'multi_hello_1' => new TypedReference('service_attr_first', MultiTagHelloNamedService::class), + 'z' => new TypedReference('service_attr_first', MultiTagHelloNamedService::class), + 'multi_hello_0' => new TypedReference('service_attr_first', MultiTagHelloNamedService::class), + ]; + $this->assertSame(array_keys($expected), array_keys($services)); + $this->assertEquals($expected, $services); + } + + public function testAttributesAreFallbacks() + { + $container = new ContainerBuilder(); + $definition = $container->register('service_attr_first', MultiTagHelloNamedService::class); + $definition->setAutoconfigured(true); + $definition->addTag('my_custom_tag', ['foo' => 'z']); + + (new ResolveInstanceofConditionalsPass())->process($container); + + $priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation(); + + $tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar'); + $services = $priorityTaggedServiceTraitImplementation->test($tag, $container); + + $this->assertEquals(['z' => new TypedReference('service_attr_first', MultiTagHelloNamedService::class)], $services); + } } class PriorityTaggedServiceTraitImplementation @@ -305,18 +332,13 @@ class HelloNamedService2 { } +#[AsTaggedItem(index: 'multi_hello_0', priority: 0)] #[AsTaggedItem(index: 'multi_hello_1', priority: 1)] #[AsTaggedItem(index: 'multi_hello_2', priority: 2)] class MultiTagHelloNamedService { } -#[AsTaggedItem(priority: 1)] -#[AsTaggedItem(priority: 2)] -class MultiNoNameTagHelloNamedService -{ -} - interface HelloInterface { public static function getFooBar(): string;