Skip to content

Commit 7eb6808

Browse files
[DependencyInjection] Add #[TaggedItem] attribute for defining the index and priority of classes found in tagged iterators/locators
1 parent 59e5ac5 commit 7eb6808

File tree

4 files changed

+97
-38
lines changed

4 files changed

+97
-38
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 and priority a service class should be found in tagged iterators/locators.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS)]
20+
class TaggedItem
21+
{
22+
public function __construct(
23+
public string $index,
24+
public ?int $priority = null,
25+
) {
26+
}
27+
}

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
* Add support for per-env configuration in loaders
1213
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration

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

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

1414
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
15+
use Symfony\Component\DependencyInjection\Attribute\TaggedItem;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1718
use Symfony\Component\DependencyInjection\Reference;
@@ -24,6 +25,8 @@
2425
*/
2526
trait PriorityTaggedServiceTrait
2627
{
28+
private $ignoreAttributesTag = 'container.ignore_attributes';
29+
2730
/**
2831
* Finds all services with the given tag name and order them by their priority.
2932
*
@@ -56,16 +59,18 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container
5659
foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
5760
$defaultPriority = null;
5861
$defaultIndex = null;
59-
$class = $container->getDefinition($serviceId)->getClass();
62+
$definition = $container->getDefinition($serviceId);
63+
$class = $definition->getClass();
6064
$class = $container->getParameterBag()->resolveValue($class) ?: null;
65+
$checkTaggedItem = !$definition->hasTag(80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() ? $this->ignoreAttributesTag : $tagName);
6166

6267
foreach ($attributes as $attribute) {
6368
$index = $priority = null;
6469

6570
if (isset($attribute['priority'])) {
6671
$priority = $attribute['priority'];
6772
} elseif (null === $defaultPriority && $defaultPriorityMethod && $class) {
68-
$defaultPriority = PriorityTaggedServiceUtil::getDefaultPriority($container, $serviceId, $class, $defaultPriorityMethod, $tagName);
73+
$defaultPriority = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem);
6974
}
7075
$priority = $priority ?? $defaultPriority ?? $defaultPriority = 0;
7176

@@ -77,7 +82,7 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container
7782
if (null !== $indexAttribute && isset($attribute[$indexAttribute])) {
7883
$index = $attribute[$indexAttribute];
7984
} elseif (null === $defaultIndex && $defaultPriorityMethod && $class) {
80-
$defaultIndex = PriorityTaggedServiceUtil::getDefaultIndex($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute);
85+
$defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
8186
}
8287
$index = $index ?? $defaultIndex ?? $defaultIndex = $serviceId;
8388

@@ -114,65 +119,55 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container
114119
class PriorityTaggedServiceUtil
115120
{
116121
/**
117-
* Gets the index defined by the default index method.
122+
* @return string|int|null
118123
*/
119-
public static function getDefaultIndex(ContainerBuilder $container, string $serviceId, string $class, string $defaultIndexMethod, string $tagName, ?string $indexAttribute): ?string
124+
public static function getDefault(ContainerBuilder $container, string $serviceId, string $class, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem)
120125
{
121-
if (!($r = $container->getReflectionClass($class)) || !$r->hasMethod($defaultIndexMethod)) {
126+
if (!($r = $container->getReflectionClass($class)) || (!$checkTaggedItem && !$r->hasMethod($defaultMethod))) {
127+
return null;
128+
}
129+
130+
if ($checkTaggedItem && !$r->hasMethod($defaultMethod)) {
131+
foreach ($r->getAttributes(TaggedItem::class) as $attribute) {
132+
return 'priority' === $indexAttribute ? $attribute->newInstance()->priority : $attribute->newInstance()->index;
133+
}
134+
122135
return null;
123136
}
124137

125138
if (null !== $indexAttribute) {
126139
$service = $class !== $serviceId ? sprintf('service "%s"', $serviceId) : 'on the corresponding service';
127-
$message = [sprintf('Either method "%s::%s()" should ', $class, $defaultIndexMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)];
140+
$message = [sprintf('Either method "%s::%s()" should ', $class, $defaultMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)];
128141
} else {
129-
$message = [sprintf('Method "%s::%s()" should ', $class, $defaultIndexMethod), '.'];
142+
$message = [sprintf('Method "%s::%s()" should ', $class, $defaultMethod), '.'];
130143
}
131144

132-
if (!($rm = $r->getMethod($defaultIndexMethod))->isStatic()) {
145+
if (!($rm = $r->getMethod($defaultMethod))->isStatic()) {
133146
throw new InvalidArgumentException(implode('be static', $message));
134147
}
135148

136149
if (!$rm->isPublic()) {
137150
throw new InvalidArgumentException(implode('be public', $message));
138151
}
139152

140-
$defaultIndex = $rm->invoke(null);
141-
142-
if (\is_int($defaultIndex)) {
143-
$defaultIndex = (string) $defaultIndex;
144-
}
145-
146-
if (!\is_string($defaultIndex)) {
147-
throw new InvalidArgumentException(implode(sprintf('return string|int (got "%s")', get_debug_type($defaultIndex)), $message));
148-
}
149-
150-
return $defaultIndex;
151-
}
153+
$default = $rm->invoke(null);
152154

153-
/**
154-
* Gets the priority defined by the default priority method.
155-
*/
156-
public static function getDefaultPriority(ContainerBuilder $container, string $serviceId, string $class, string $defaultPriorityMethod, string $tagName): ?int
157-
{
158-
if (!($r = $container->getReflectionClass($class)) || !$r->hasMethod($defaultPriorityMethod)) {
159-
return null;
160-
}
155+
if ('priority' === $indexAttribute) {
156+
if (!\is_int($default)) {
157+
throw new InvalidArgumentException(implode(sprintf('return int (got "%s")', get_debug_type($default)), $message));
158+
}
161159

162-
if (!($rm = $r->getMethod($defaultPriorityMethod))->isStatic()) {
163-
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));
160+
return $default;
164161
}
165162

166-
if (!$rm->isPublic()) {
167-
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));
163+
if (\is_int($default)) {
164+
$default = (string) $default;
168165
}
169166

170-
$defaultPriority = $rm->invoke(null);
171-
172-
if (!\is_int($defaultPriority)) {
173-
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));
167+
if (!\is_string($default)) {
168+
throw new InvalidArgumentException(implode(sprintf('return string|int (got "%s")', get_debug_type($default)), $message));
174169
}
175170

176-
return $defaultPriority;
171+
return $default;
177172
}
178173
}

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

Lines changed: 36 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;
@@ -188,6 +191,34 @@ public function provideInvalidDefaultMethods(): iterable
188191
yield ['getMethodShouldBePublicInsteadPrivate', null, sprintf('Method "%s::getMethodShouldBePublicInsteadPrivate()" should be public.', FooTaggedForInvalidDefaultMethodClass::class)];
189192
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)];
190193
}
194+
195+
/**
196+
* @requires PHP 8
197+
*/
198+
public function testTaggedItemAttributes()
199+
{
200+
$container = new ContainerBuilder();
201+
$container->register('service1', FooTagClass::class)->addTag('my_custom_tag');
202+
$container->register('service2', HelloNamedService::class)
203+
->setAutoconfigured(true)
204+
->setInstanceofConditionals([
205+
HelloNamedService::class => (new ChildDefinition(''))->addTag('my_custom_tag'),
206+
\stdClass::class => (new ChildDefinition(''))->addTag('my_custom_tag2'),
207+
]);
208+
209+
(new ResolveInstanceofConditionalsPass())->process($container);
210+
211+
$priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation();
212+
213+
$tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar');
214+
$expected = [
215+
'hello' => new TypedReference('service2', HelloNamedService::class),
216+
'service1' => new TypedReference('service1', FooTagClass::class),
217+
];
218+
$services = $priorityTaggedServiceTraitImplementation->test($tag, $container);
219+
$this->assertSame(array_keys($expected), array_keys($services));
220+
$this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container));
221+
}
191222
}
192223

193224
class PriorityTaggedServiceTraitImplementation
@@ -199,3 +230,8 @@ public function test($tagName, ContainerBuilder $container)
199230
return $this->findAndSortTaggedServices($tagName, $container);
200231
}
201232
}
233+
234+
#[TaggedItem(index: 'hello', priority: 1)]
235+
class HelloNamedService extends \stdClass
236+
{
237+
}

0 commit comments

Comments
 (0)