Skip to content

Commit 6163a20

Browse files
[DependencyInjection] Add #[TaggedItem] attribute for defining the index and priority of classes found in tagged iterators/locators
1 parent 8aaa152 commit 6163a20

File tree

5 files changed

+109
-1
lines changed

5 files changed

+109
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Attribute;
13+
14+
/**
15+
* An attribute to tell under which index a class should be found in tagged iterators/locators.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
20+
class TaggedItem
21+
{
22+
public function __construct(
23+
public string $index,
24+
public ?int $priority = null,
25+
public ?string $type = null,
26+
) {
27+
}
28+
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `ServicesConfigurator::remove()` in the PHP-DSL
88
* Add `%env(not:...)%` processor to negate boolean values
99
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
10+
* Add `#[TaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators
1011
* Add autoconfigurable attributes
1112

1213
5.2.0

src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container
7676

7777
if (null !== $indexAttribute && isset($attribute[$indexAttribute])) {
7878
$index = $attribute[$indexAttribute];
79+
} elseif (isset($attribute['name'])) {
80+
$index = $attribute['name'];
7981
} elseif (null === $defaultIndex && $defaultPriorityMethod && $class) {
8082
$defaultIndex = PriorityTaggedServiceUtil::getDefaultIndex($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute);
8183
}

src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\Attribute\TaggedItem;
1415
use Symfony\Component\DependencyInjection\ChildDefinition;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Definition;
@@ -24,6 +25,13 @@
2425
*/
2526
class ResolveInstanceofConditionalsPass implements CompilerPassInterface
2627
{
28+
private $ignoreAttributesTag;
29+
30+
public function __construct(string $ignoreAttributesTag = 'container.ignore_attributes')
31+
{
32+
$this->ignoreAttributesTag = $ignoreAttributesTag;
33+
}
34+
2735
/**
2836
* {@inheritdoc}
2937
*/
@@ -72,6 +80,14 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi
7280
$reflectionClass = null;
7381
$parent = $definition instanceof ChildDefinition ? $definition->getParent() : null;
7482

83+
$typedTaggedItems = [];
84+
if (80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() && !$definition->hasTag($this->ignoreAttributesTag) && $reflectionClass = $container->getReflectionClass($class, false) ?: false) {
85+
foreach ($reflectionClass->getAttributes(TaggedItem::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
86+
$taggedItem = $attribute->newInstance();
87+
$typedTaggedItems += [$taggedItem->type ?? '' => $taggedItem];
88+
}
89+
}
90+
7591
foreach ($conditionals as $interface => $instanceofDefs) {
7692
if ($interface !== $class && !(null === $reflectionClass ? $reflectionClass = ($container->getReflectionClass($class, false) ?: false) : $reflectionClass)) {
7793
continue;
@@ -87,7 +103,22 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi
87103
$instanceofDef->setAbstract(true)->setParent($parent ?: '.abstract.instanceof.'.$id);
88104
$parent = '.instanceof.'.$interface.'.'.$key.'.'.$id;
89105
$container->setDefinition($parent, $instanceofDef);
90-
$instanceofTags[] = $instanceofDef->getTags();
106+
107+
$tags = $instanceofDef->getTags();
108+
if ($tags && $taggedItem = $typedTaggedItems[$interface] ?? $typedTaggedItems[''] ?? null) {
109+
foreach ($tags as $tag => $attributes) {
110+
foreach ($attributes as $i => $attribute) {
111+
if (null !== $taggedItem->index) {
112+
$tags[$tag][$i] += ['name' => $taggedItem->index];
113+
}
114+
if (null !== $taggedItem->priority) {
115+
$tags[$tag][$i] += ['priority' => $taggedItem->priority];
116+
}
117+
}
118+
}
119+
}
120+
121+
$instanceofTags[] = $tags;
91122
$instanceofBindings = $instanceofDef->getBindings() + $instanceofBindings;
92123

93124
foreach ($instanceofDef->getMethodCalls() as $methodCall) {

src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16+
use Symfony\Component\DependencyInjection\Attribute\TaggedItem;
17+
use Symfony\Component\DependencyInjection\ChildDefinition;
1618
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
19+
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
1720
use Symfony\Component\DependencyInjection\ContainerBuilder;
1821
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1922
use Symfony\Component\DependencyInjection\Reference;
@@ -184,6 +187,43 @@ public function provideInvalidDefaultMethods(): iterable
184187
yield ['getMethodShouldBePublicInsteadPrivate', null, sprintf('Method "%s::getMethodShouldBePublicInsteadPrivate()" should be public.', FooTaggedForInvalidDefaultMethodClass::class)];
185188
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)];
186189
}
190+
191+
/**
192+
* @requires PHP 8
193+
*/
194+
public function testTaggedItemAttributes()
195+
{
196+
$container = new ContainerBuilder();
197+
$container->register('service1', FooTagClass::class)->addTag('my_custom_tag');
198+
$container->register('service2', HelloNamedService::class)
199+
->setAutoconfigured(true)
200+
->setInstanceofConditionals([
201+
HelloNamedService::class => (new ChildDefinition(''))->addTag('my_custom_tag'),
202+
\stdClass::class => (new ChildDefinition(''))->addTag('my_custom_tag2'),
203+
]);
204+
205+
(new ResolveInstanceofConditionalsPass())->process($container);
206+
207+
$priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation();
208+
209+
$tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar');
210+
$expected = [
211+
'hello' => new TypedReference('service2', HelloNamedService::class),
212+
'service1' => new TypedReference('service1', FooTagClass::class),
213+
];
214+
$services = $priorityTaggedServiceTraitImplementation->test($tag, $container);
215+
$this->assertSame(array_keys($expected), array_keys($services));
216+
$this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container));
217+
218+
$tag = new TaggedIteratorArgument('my_custom_tag2', 'foo', 'getFooBar');
219+
$expected = [
220+
'world' => new TypedReference('service2', HelloNamedService::class),
221+
];
222+
$services = $priorityTaggedServiceTraitImplementation->test($tag, $container);
223+
$this->assertSame(array_keys($expected), array_keys($services));
224+
$this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container));
225+
226+
}
187227
}
188228

189229
class PriorityTaggedServiceTraitImplementation
@@ -195,3 +235,9 @@ public function test($tagName, ContainerBuilder $container)
195235
return $this->findAndSortTaggedServices($tagName, $container);
196236
}
197237
}
238+
239+
#[TaggedItem(index: 'hello', priority: 1)]
240+
#[TaggedItem(index: 'world', type: 'stdClass')]
241+
class HelloNamedService extends \stdClass
242+
{
243+
}

0 commit comments

Comments
 (0)