diff --git a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php index c33e8615b254e..bfe9787f7c9bd 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php @@ -24,6 +24,7 @@ class TaggedIteratorArgument extends IteratorArgument private ?string $defaultPriorityMethod; private bool $needsIndexes; private array $exclude; + private bool $excludeSelf = true; /** * @param string $tag The name of the tag identifying the target services @@ -32,8 +33,9 @@ class TaggedIteratorArgument extends IteratorArgument * @param bool $needsIndexes Whether indexes are required and should be generated when computing the map * @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 * @param array $exclude Services to exclude from the iterator + * @param bool $excludeSelf Whether to automatically exclude the referencing service from the iterator */ - public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $needsIndexes = false, string $defaultPriorityMethod = null, array $exclude = []) + public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $needsIndexes = false, string $defaultPriorityMethod = null, array $exclude = [], bool $excludeSelf = true) { parent::__construct([]); @@ -47,6 +49,7 @@ public function __construct(string $tag, string $indexAttribute = null, string $ $this->needsIndexes = $needsIndexes; $this->defaultPriorityMethod = $defaultPriorityMethod ?: ($indexAttribute ? 'getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Priority' : null); $this->exclude = $exclude; + $this->excludeSelf = $excludeSelf; } public function getTag() @@ -78,4 +81,9 @@ public function getExclude(): array { return $this->exclude; } + + public function excludeSelf(): bool + { + return $this->excludeSelf; + } } diff --git a/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php b/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php index 5898a6afe0e81..fb33fb572942b 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php @@ -20,6 +20,7 @@ public function __construct( public ?string $defaultIndexMethod = null, public ?string $defaultPriorityMethod = null, public string|array $exclude = [], + public bool $excludeSelf = true, ) { } } diff --git a/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php b/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php index b706a6388bf0d..f05ae53bc4284 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php @@ -20,6 +20,7 @@ public function __construct( public ?string $defaultIndexMethod = null, public ?string $defaultPriorityMethod = null, public string|array $exclude = [], + public bool $excludeSelf = true, ) { } } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 81dec56e681f3..df045b56f05c8 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -24,6 +24,8 @@ CHANGELOG * Deprecate using numeric parameter names * Add support for tagged iterators/locators `exclude` option to the xml and yaml loaders/dumpers * Allow injecting `string $env` into php config closures + * Add `excludeSelf` parameter to `TaggedIteratorArgument` with default value to `true` + to control whether the referencing service should be automatically excluded from the iterator 6.1 --- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 66a175d76c267..ac94cd7ae5b24 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -88,11 +88,11 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed } if ($value instanceof TaggedIterator) { - return new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, false, $value->defaultPriorityMethod, (array) $value->exclude); + return new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, false, $value->defaultPriorityMethod, (array) $value->exclude, $value->excludeSelf); } if ($value instanceof TaggedLocator) { - return new ServiceLocatorArgument(new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, true, $value->defaultPriorityMethod, (array) $value->exclude)); + return new ServiceLocatorArgument(new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, true, $value->defaultPriorityMethod, (array) $value->exclude, $value->excludeSelf)); } if ($value instanceof MapDecorated) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php index 309bf63118d4e..2ddcaa0c08d8c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php @@ -37,9 +37,8 @@ trait PriorityTaggedServiceTrait * * @return Reference[] */ - private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagName, ContainerBuilder $container): array + private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagName, ContainerBuilder $container, array $exclude = []): array { - $exclude = []; $indexAttribute = $defaultIndexMethod = $needsIndexes = $defaultPriorityMethod = null; if ($tagName instanceof TaggedIteratorArgument) { @@ -47,7 +46,7 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam $defaultIndexMethod = $tagName->getDefaultIndexMethod(); $needsIndexes = $tagName->needsIndexes(); $defaultPriorityMethod = $tagName->getDefaultPriorityMethod() ?? 'getDefaultPriority'; - $exclude = $tagName->getExclude(); + $exclude = array_merge($exclude, $tagName->getExclude()); $tagName = $tagName->getTag(); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveTaggedIteratorArgumentPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveTaggedIteratorArgumentPass.php index 1fca5ebaa5f8a..469d001b51fea 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveTaggedIteratorArgumentPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveTaggedIteratorArgumentPass.php @@ -28,7 +28,12 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed return parent::processValue($value, $isRoot); } - $value->setValues($this->findAndSortTaggedServices($value, $this->container)); + $exclude = $value->getExclude(); + if ($value->excludeSelf()) { + $exclude[] = $this->currentId; + } + + $value->setValues($this->findAndSortTaggedServices($value, $this->container, $exclude)); return $value; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 7c0ea3261300b..4acbfe56a9aa3 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -550,7 +550,7 @@ private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file $excludes = [$arg->getAttribute('exclude')]; } - $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator, $arg->getAttribute('default-priority-method') ?: null, $excludes); + $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator, $arg->getAttribute('default-priority-method') ?: null, $excludes, $arg->getAttribute('exclude-self') ?: true); if ($forLocator) { $arguments[$key] = new ServiceLocatorArgument($arguments[$key]); diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index ad61c14437d1a..a9e35fdad654c 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -824,11 +824,11 @@ private function resolveServices(mixed $value, string $file, bool $isParameter = $forLocator = 'tagged_locator' === $value->getTag(); if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) { - if ($diff = array_diff(array_keys($argument), $supportedKeys = ['tag', 'index_by', 'default_index_method', 'default_priority_method', 'exclude'])) { + if ($diff = array_diff(array_keys($argument), $supportedKeys = ['tag', 'index_by', 'default_index_method', 'default_priority_method', 'exclude', 'exclude_self'])) { throw new InvalidArgumentException(sprintf('"!%s" tag contains unsupported key "%s"; supported ones are "%s".', $value->getTag(), implode('", "', $diff), implode('", "', $supportedKeys))); } - $argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null, $forLocator, $argument['default_priority_method'] ?? null, (array) ($argument['exclude'] ?? null)); + $argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null, $forLocator, $argument['default_priority_method'] ?? null, (array) ($argument['exclude'] ?? null), $argument['exclude_self'] ?? true); } elseif (\is_string($argument) && $argument) { $argument = new TaggedIteratorArgument($argument, null, null, $forLocator); } else { diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index 20e97866788b6..83e430a859445 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -302,6 +302,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index ba979be80bc04..4da06e889b715 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -462,7 +462,7 @@ public static function getSubscribedServices(): array 'autowired' => new ServiceClosureArgument(new Reference('service.id')), 'autowired.nullable' => new ServiceClosureArgument(new Reference('service.id', ContainerInterface::NULL_ON_INVALID_REFERENCE)), 'autowired.parameter' => new ServiceClosureArgument('foobar'), - 'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.oZHAdom.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), + 'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.LnJLtj2.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), 'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'someTarget', [new Target('someTarget')])), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveTaggedIteratorArgumentPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveTaggedIteratorArgumentPassTest.php index a62a585c6ef0c..7e2fa2f7ddda1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveTaggedIteratorArgumentPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveTaggedIteratorArgumentPassTest.php @@ -54,4 +54,36 @@ public function testProcessWithIndexes() $expected->setValues(['1' => new TypedReference('service_a', 'stdClass'), '2' => new TypedReference('service_b', 'stdClass')]); $this->assertEquals($expected, $properties['foos']); } + + public function testProcesWithAutoExcludeReferencingService() + { + $container = new ContainerBuilder(); + $container->register('service_a', 'stdClass')->addTag('foo', ['key' => '1']); + $container->register('service_b', 'stdClass')->addTag('foo', ['key' => '2']); + $container->register('service_c', 'stdClass')->addTag('foo', ['key' => '3'])->setProperty('foos', new TaggedIteratorArgument('foo', 'key')); + + (new ResolveTaggedIteratorArgumentPass())->process($container); + + $properties = $container->getDefinition('service_c')->getProperties(); + + $expected = new TaggedIteratorArgument('foo', 'key'); + $expected->setValues(['1' => new TypedReference('service_a', 'stdClass'), '2' => new TypedReference('service_b', 'stdClass')]); + $this->assertEquals($expected, $properties['foos']); + } + + public function testProcesWithoutAutoExcludeReferencingService() + { + $container = new ContainerBuilder(); + $container->register('service_a', 'stdClass')->addTag('foo', ['key' => '1']); + $container->register('service_b', 'stdClass')->addTag('foo', ['key' => '2']); + $container->register('service_c', 'stdClass')->addTag('foo', ['key' => '3'])->setProperty('foos', new TaggedIteratorArgument(tag: 'foo', indexAttribute: 'key', excludeSelf: false)); + + (new ResolveTaggedIteratorArgumentPass())->process($container); + + $properties = $container->getDefinition('service_c')->getProperties(); + + $expected = new TaggedIteratorArgument(tag: 'foo', indexAttribute: 'key', excludeSelf: false); + $expected->setValues(['1' => new TypedReference('service_a', 'stdClass'), '2' => new TypedReference('service_b', 'stdClass'), '3' => new TypedReference('service_c', 'stdClass')]); + $this->assertEquals($expected, $properties['foos']); + } }