Skip to content

Commit 71a3511

Browse files
committed
Add exclude to TaggedIterator and TaggedLocator
With this change, you can exclude one or more services from a tagged iterator or locator. It's common when working with Delegator or Chain services that call a list of services that implement an interface, to also want to implement the interface on Delegator/Chain. An example of this is in the Symfony HttpKernel component: https://github.com/symfony/symfony/blob/0d6e859db236e37b8baa2b2bf4c8b7d14d151570/src/Symfony/Component/HttpKernel/CacheClearer/CacheClearerInterface.php#L19-L25 https://github.com/symfony/symfony/blob/0d6e859db236e37b8baa2b2bf4c8b7d14d151570/src/Symfony/Component/HttpKernel/CacheClearer/ChainCacheClearer.php#L21-L31 The `ChainCacheClearer` implements `CacheClearerInterface` and calls a list of `CacheClearerInterface` clearers. If that would have been configured with `#[AutoconfigureTag]` and `#[TaggedIterator]`, the `ChainCacheClearer` would receive itself in the `iterable $clearers`. With this change, it can exclude itself and rely fully on autowire/configure. Another example: ```php #[AutoconfigureTag] interface ErrorTracker { public function trackError(string $error): void; } final class SentryErrorTracker implement ErrorTracker { public function trackError(string $error): void { echo "Send error to Sentry\n"; } } final class NewRelicErrorTracker implement ErrorTracker { public function trackError(string $error): void { echo "Send error to NewRelic\n"; } } final class DelegatingErrorTracker implements ErrorTracker { public function __construct( #[TaggedIterator(ErrorTracker::class, exclude: self::class)] private iterable $trackers ) {} public function trackError(string $error): void { foreach($this->trackers as $tracker) { $tracker->trackError($error); } } } // Alias ErrorTracker interface to DelegatingErrorTracker service final class MyController { public function __construct(private ErrorTracker $errorTracker) {} public function __invoke() { $this->errorTracker->trackError('Hello, World!'); } } ``` Without this change, the `DelegatingErrorTracker` would receive itself in the tagged iterator.
1 parent b0f57d1 commit 71a3511

13 files changed

+158
-8
lines changed

Argument/TaggedIteratorArgument.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,17 @@ class TaggedIteratorArgument extends IteratorArgument
2323
private ?string $defaultIndexMethod;
2424
private ?string $defaultPriorityMethod;
2525
private bool $needsIndexes;
26+
private array $exclude;
2627

2728
/**
2829
* @param string $tag The name of the tag identifying the target services
2930
* @param string|null $indexAttribute The name of the attribute that defines the key referencing each service in the tagged collection
3031
* @param string|null $defaultIndexMethod The static method that should be called to get each service's key when their tag doesn't define the previous attribute
3132
* @param bool $needsIndexes Whether indexes are required and should be generated when computing the map
3233
* @param string|null $defaultPriorityMethod The static method that should be called to get each service's priority when their tag doesn't define the "priority" attribute
34+
* @param array $exclude Services to exclude from the iterator
3335
*/
34-
public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $needsIndexes = false, string $defaultPriorityMethod = null)
36+
public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $needsIndexes = false, string $defaultPriorityMethod = null, array $exclude = [])
3537
{
3638
parent::__construct([]);
3739

@@ -44,6 +46,7 @@ public function __construct(string $tag, string $indexAttribute = null, string $
4446
$this->defaultIndexMethod = $defaultIndexMethod ?: ($indexAttribute ? 'getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Name' : null);
4547
$this->needsIndexes = $needsIndexes;
4648
$this->defaultPriorityMethod = $defaultPriorityMethod ?: ($indexAttribute ? 'getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Priority' : null);
49+
$this->exclude = $exclude;
4750
}
4851

4952
public function getTag()
@@ -70,4 +73,9 @@ public function getDefaultPriorityMethod(): ?string
7073
{
7174
return $this->defaultPriorityMethod;
7275
}
76+
77+
public function getExclude(): array
78+
{
79+
return $this->exclude;
80+
}
7381
}

Attribute/TaggedIterator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public function __construct(
1919
public ?string $indexAttribute = null,
2020
public ?string $defaultIndexMethod = null,
2121
public ?string $defaultPriorityMethod = null,
22+
public string|array $exclude = [],
2223
) {
2324
}
2425
}

Attribute/TaggedLocator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public function __construct(
1919
public ?string $indexAttribute = null,
2020
public ?string $defaultIndexMethod = null,
2121
public ?string $defaultPriorityMethod = null,
22+
public string|array $exclude = [],
2223
) {
2324
}
2425
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
6.1
5+
---
6+
7+
* Add `$exclude` to `TaggedIterator` and `TaggedLocator` attributes
8+
* Add `$exclude` to `tagged_iterator` and `tagged_locator` configurator
9+
410
6.0
511
---
612

Compiler/AutowirePass.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,13 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
247247
foreach ($parameter->getAttributes() as $attribute) {
248248
if (TaggedIterator::class === $attribute->getName()) {
249249
$attribute = $attribute->newInstance();
250-
$arguments[$index] = new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, false, $attribute->defaultPriorityMethod);
250+
$arguments[$index] = new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, false, $attribute->defaultPriorityMethod, (array) $attribute->exclude);
251251
break;
252252
}
253253

254254
if (TaggedLocator::class === $attribute->getName()) {
255255
$attribute = $attribute->newInstance();
256-
$arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod));
256+
$arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod, (array) $attribute->exclude));
257257
break;
258258
}
259259
}

Compiler/PriorityTaggedServiceTrait.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,26 @@ trait PriorityTaggedServiceTrait
3939
*/
4040
private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagName, ContainerBuilder $container): array
4141
{
42+
$exclude = [];
4243
$indexAttribute = $defaultIndexMethod = $needsIndexes = $defaultPriorityMethod = null;
4344

4445
if ($tagName instanceof TaggedIteratorArgument) {
4546
$indexAttribute = $tagName->getIndexAttribute();
4647
$defaultIndexMethod = $tagName->getDefaultIndexMethod();
4748
$needsIndexes = $tagName->needsIndexes();
4849
$defaultPriorityMethod = $tagName->getDefaultPriorityMethod() ?? 'getDefaultPriority';
50+
$exclude = $tagName->getExclude();
4951
$tagName = $tagName->getTag();
5052
}
5153

5254
$i = 0;
5355
$services = [];
5456

5557
foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
58+
if (\in_array($serviceId, $exclude, true)) {
59+
continue;
60+
}
61+
5662
$defaultPriority = null;
5763
$defaultIndex = null;
5864
$definition = $container->getDefinition($serviceId);

Loader/Configurator/ContainerConfigurator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,17 @@ function iterator(array $values): IteratorArgument
139139
/**
140140
* Creates a lazy iterator by tag name.
141141
*/
142-
function tagged_iterator(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, string $defaultPriorityMethod = null): TaggedIteratorArgument
142+
function tagged_iterator(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, string $defaultPriorityMethod = null, string|array $exclude = []): TaggedIteratorArgument
143143
{
144-
return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod);
144+
return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude);
145145
}
146146

147147
/**
148148
* Creates a service locator by tag name.
149149
*/
150-
function tagged_locator(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, string $defaultPriorityMethod = null): ServiceLocatorArgument
150+
function tagged_locator(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, string $defaultPriorityMethod = null, string|array $exclude = []): ServiceLocatorArgument
151151
{
152-
return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod));
152+
return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude));
153153
}
154154

155155
/**

Tests/Compiler/IntegrationTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomMethodAttribute;
2929
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomParameterAttribute;
3030
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomPropertyAttribute;
31+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface2;
32+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredService1;
33+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredService2;
3134
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
3235
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
3336
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
@@ -43,6 +46,7 @@
4346
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerWithDefaultIndexMethodAndWithDefaultPriorityMethod;
4447
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerWithDefaultPriorityMethod;
4548
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerWithoutIndex;
49+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedConsumerWithExclude;
4650
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1;
4751
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
4852
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
@@ -999,6 +1003,44 @@ static function (ChildDefinition $definition) {
9991003
self::assertSame(6, $service->sum);
10001004
self::assertTrue($service->hasBeenConfigured);
10011005
}
1006+
1007+
public function testTaggedIteratorAndLocatorWithExclude()
1008+
{
1009+
$container = new ContainerBuilder();
1010+
1011+
$container->register(AutoconfiguredService1::class)
1012+
->addTag(AutoconfiguredInterface2::class)
1013+
->setPublic(true)
1014+
;
1015+
$container->register(AutoconfiguredService2::class)
1016+
->addTag(AutoconfiguredInterface2::class)
1017+
->setPublic(true)
1018+
;
1019+
$container->register(TaggedConsumerWithExclude::class)
1020+
->addTag(AutoconfiguredInterface2::class)
1021+
->setAutoconfigured(true)
1022+
->setAutowired(true)
1023+
->setPublic(true)
1024+
;
1025+
1026+
$container->compile();
1027+
1028+
$this->assertTrue($container->getDefinition(AutoconfiguredService1::class)->hasTag(AutoconfiguredInterface2::class));
1029+
$this->assertTrue($container->getDefinition(AutoconfiguredService2::class)->hasTag(AutoconfiguredInterface2::class));
1030+
$this->assertTrue($container->getDefinition(TaggedConsumerWithExclude::class)->hasTag(AutoconfiguredInterface2::class));
1031+
1032+
$s = $container->get(TaggedConsumerWithExclude::class);
1033+
1034+
$items = iterator_to_array($s->items->getIterator());
1035+
$this->assertCount(2, $items);
1036+
$this->assertInstanceOf(AutoconfiguredService1::class, $items[0]);
1037+
$this->assertInstanceOf(AutoconfiguredService2::class, $items[1]);
1038+
1039+
$locator = $s->locator;
1040+
$this->assertTrue($locator->has(AutoconfiguredService1::class));
1041+
$this->assertTrue($locator->has(AutoconfiguredService2::class));
1042+
$this->assertFalse($locator->has(TaggedConsumerWithExclude::class));
1043+
}
10021044
}
10031045

10041046
class ServiceSubscriberStub implements ServiceSubscriberInterface

Tests/Compiler/PriorityTaggedServiceTraitTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,18 @@ public function testTaggedItemAttributes()
205205
$container->register('service3', HelloNamedService2::class)
206206
->setAutoconfigured(true)
207207
->addTag('my_custom_tag');
208+
$container->register('service4', HelloNamedService2::class)
209+
->setAutoconfigured(true)
210+
->addTag('my_custom_tag');
211+
$container->register('service5', HelloNamedService2::class)
212+
->setAutoconfigured(true)
213+
->addTag('my_custom_tag');
208214

209215
(new ResolveInstanceofConditionalsPass())->process($container);
210216

211217
$priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation();
212218

213-
$tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar');
219+
$tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar', exclude: ['service4', 'service5']);
214220
$expected = [
215221
'service3' => new TypedReference('service3', HelloNamedService2::class),
216222
'hello' => new TypedReference('service2', HelloNamedService::class),
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Tests\Fixtures;
13+
14+
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
15+
16+
#[AutoconfigureTag]
17+
interface AutoconfiguredInterface2
18+
{
19+
}

0 commit comments

Comments
 (0)