diff --git a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php index a3099686d1..f3b323ce4c 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php @@ -21,22 +21,22 @@ class TaggedIteratorArgument extends IteratorArgument private $tag; private $indexAttribute; private $defaultIndexMethod; + private $useFqcnAsFallback = false; /** * @param string $tag The name of the tag identifying the target services * @param string|null $indexAttribute The name of the attribute that defines the key referencing each service in the tagged collection * @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 + * @param bool $useFqcnAsFallback Whether the FQCN of the service should be used as index when neither the attribute nor the method are defined */ - public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null) + public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $useFqcnAsFallback = false) { parent::__construct([]); $this->tag = $tag; - - if ($indexAttribute) { - $this->indexAttribute = $indexAttribute; - $this->defaultIndexMethod = $defaultIndexMethod ?: ('getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Name'); - } + $this->indexAttribute = $indexAttribute; + $this->defaultIndexMethod = $defaultIndexMethod ?: ('getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute ?? ''))).'Name'); + $this->useFqcnAsFallback = $useFqcnAsFallback; } public function getTag() @@ -53,4 +53,9 @@ class TaggedIteratorArgument extends IteratorArgument { return $this->defaultIndexMethod; } + + public function useFqcnAsFallback(): bool + { + return $this->useFqcnAsFallback; + } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php index f9046d3c32..3bf8e3be18 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; /** * Trait that allows a generic method to find and sort service by priority option in the tag. @@ -40,34 +41,48 @@ trait PriorityTaggedServiceTrait */ private function findAndSortTaggedServices($tagName, ContainerBuilder $container) { - $indexAttribute = $defaultIndexMethod = null; + $indexAttribute = $defaultIndexMethod = $useFqcnAsFallback = null; + if ($tagName instanceof TaggedIteratorArgument) { $indexAttribute = $tagName->getIndexAttribute(); $defaultIndexMethod = $tagName->getDefaultIndexMethod(); + $useFqcnAsFallback = $tagName->useFqcnAsFallback(); $tagName = $tagName->getTag(); } + $services = []; foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) { $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; - if (null === $indexAttribute) { + if (null === $indexAttribute && !$useFqcnAsFallback) { $services[$priority][] = new Reference($serviceId); continue; } - if (isset($attributes[0][$indexAttribute])) { - $services[$priority][$attributes[0][$indexAttribute]] = new Reference($serviceId); + $class = $container->getDefinition($serviceId)->getClass(); + $class = $container->getParameterBag()->resolveValue($class) ?: null; + + if (null !== $indexAttribute && isset($attributes[0][$indexAttribute])) { + $services[$priority][$attributes[0][$indexAttribute]] = new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $attributes[0][$indexAttribute]); continue; } - if (!$r = $container->getReflectionClass($class = $container->getDefinition($serviceId)->getClass())) { + if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $serviceId)); } + $class = $r->name; + if (!$r->hasMethod($defaultIndexMethod)) { + if ($useFqcnAsFallback) { + $services[$priority][$class] = new TypedReference($serviceId, $class); + + continue; + } + throw new InvalidArgumentException(sprintf('Method "%s::%s()" not found: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, $tagName, $serviceId, $indexAttribute)); } @@ -85,7 +100,7 @@ trait PriorityTaggedServiceTrait throw new InvalidArgumentException(sprintf('Method "%s::%s()" should return a string, got %s: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, \gettype($key), $tagName, $serviceId, $indexAttribute)); } - $services[$priority][$key] = new Reference($serviceId); + $services[$priority][$key] = new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $key); } if ($services) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index 5b9edd0351..57ac2675ae 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -280,36 +280,26 @@ class XmlDumper extends Dumper if ($value instanceof ServiceClosureArgument) { $value = $value->getValues()[0]; } - if (\is_array($value)) { + if (\is_array($tag = $value)) { $element->setAttribute('type', 'collection'); $this->convertParameters($value, $type, $element, 'key'); - } elseif ($value instanceof TaggedIteratorArgument) { - $element->setAttribute('type', 'tagged'); - $element->setAttribute('tag', $value->getTag()); + } elseif ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) { + $element->setAttribute('type', $value instanceof TaggedIteratorArgument ? 'tagged' : 'tagged_locator'); + $element->setAttribute('tag', $tag->getTag()); - if (null !== $value->getIndexAttribute()) { - $element->setAttribute('index-by', $value->getIndexAttribute()); - } + if (null !== $tag->getIndexAttribute()) { + $element->setAttribute('index-by', $tag->getIndexAttribute()); - if (null !== $value->getDefaultIndexMethod()) { - $element->setAttribute('default-index-method', $value->getDefaultIndexMethod()); + if (null !== $tag->getDefaultIndexMethod()) { + $element->setAttribute('default-index-method', $tag->getDefaultIndexMethod()); + } } } elseif ($value instanceof IteratorArgument) { $element->setAttribute('type', 'iterator'); $this->convertParameters($value->getValues(), $type, $element, 'key'); } elseif ($value instanceof ServiceLocatorArgument) { - if ($value->getTaggedIteratorArgument()) { - $element->setAttribute('type', 'tagged_locator'); - $element->setAttribute('tag', $value->getTaggedIteratorArgument()->getTag()); - $element->setAttribute('index-by', $value->getTaggedIteratorArgument()->getIndexAttribute()); - - if (null !== $value->getTaggedIteratorArgument()->getDefaultIndexMethod()) { - $element->setAttribute('default-index-method', $value->getTaggedIteratorArgument()->getDefaultIndexMethod()); - } - } else { - $element->setAttribute('type', 'service_locator'); - $this->convertParameters($value->getValues(), $type, $element, 'key'); - } + $element->setAttribute('type', 'service_locator'); + $this->convertParameters($value->getValues(), $type, $element, 'key'); } elseif ($value instanceof Reference) { $element->setAttribute('type', 'service'); $element->setAttribute('id', (string) $value); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index b4c899379c..89dae636de 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -232,38 +232,29 @@ class YamlDumper extends Dumper $value = $value->getValues()[0]; } if ($value instanceof ArgumentInterface) { - if ($value instanceof TaggedIteratorArgument) { - if (null !== $value->getIndexAttribute()) { - $taggedValueContent = [ - 'tag' => $value->getTag(), - 'index_by' => $value->getIndexAttribute(), + $tag = $value; + + if ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) { + if (null === $tag->getIndexAttribute()) { + $content = $tag->getTag(); + } else { + $content = [ + 'tag' => $tag->getTag(), + 'index_by' => $tag->getIndexAttribute(), ]; - if (null !== $value->getDefaultIndexMethod()) { - $taggedValueContent['default_index_method'] = $value->getDefaultIndexMethod(); + if (null !== $tag->getDefaultIndexMethod()) { + $content['default_index_method'] = $tag->getDefaultIndexMethod(); } - - return new TaggedValue('tagged', $taggedValueContent); } - return new TaggedValue('tagged', $value->getTag()); + return new TaggedValue($value instanceof TaggedIteratorArgument ? 'tagged' : 'tagged_locator', $content); } + if ($value instanceof IteratorArgument) { $tag = 'iterator'; } elseif ($value instanceof ServiceLocatorArgument) { $tag = 'service_locator'; - if ($value->getTaggedIteratorArgument()) { - $taggedValueContent = [ - 'tag' => $value->getTaggedIteratorArgument()->getTag(), - 'index_by' => $value->getTaggedIteratorArgument()->getIndexAttribute(), - ]; - - if (null !== $value->getTaggedIteratorArgument()->getDefaultIndexMethod()) { - $taggedValueContent['default_index_method'] = $value->getTaggedIteratorArgument()->getDefaultIndexMethod(); - } - - return new TaggedValue('tagged_locator', $taggedValueContent); - } } else { throw new RuntimeException(sprintf('Unspecified Yaml tag for type "%s".', \get_class($value))); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php index d4b03fc2c1..87b066faf5 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -126,7 +126,7 @@ function tagged(string $tag, string $indexAttribute = null, string $defaultIndex */ function tagged_locator(string $tag, string $indexAttribute, string $defaultIndexMethod = null): ServiceLocatorArgument { - return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod)); + return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true)); } /** diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index ced8b0142d..92c27e6ebf 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -533,19 +533,20 @@ class XmlFileLoader extends FileLoader throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service_locator" only accepts maps of type="service" references in "%s".', $name, $file)); } break; - case 'tagged_locator': - if (!$arg->getAttribute('tag') || !$arg->getAttribute('index-by')) { - throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged_locator" has no or empty "tag" or "index-by" attributes in "%s".', $name, $file)); - } - - $arguments[$key] = new ServiceLocatorArgument(new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by'), $arg->getAttribute('default-index-method') ?: null)); - break; case 'tagged': + case 'tagged_locator': + $type = $arg->getAttribute('type'); + $forLocator = 'tagged_locator' === $type; + if (!$arg->getAttribute('tag')) { - throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged" has no or empty "tag" attribute in "%s".', $name, $file)); + throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="%s" has no or empty "tag" attribute in "%s".', $name, $type, $file)); } - $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null); + $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator); + + if ($forLocator) { + $arguments[$key] = new ServiceLocatorArgument($arguments[$key]); + } break; case 'binary': if (false === $value = base64_decode($arg->nodeValue)) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 4d9da4e314..ecd66dd276 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -711,31 +711,28 @@ class YamlFileLoader extends FileLoader throw new InvalidArgumentException(sprintf('"!service_locator" tag only accepts maps of "@service" references in "%s".', $file)); } } - if ('tagged' === $value->getTag()) { + if (\in_array($value->getTag(), ['tagged', 'tagged_locator'], true)) { + $forLocator = 'tagged_locator' === $value->getTag(); + if (\is_string($argument) && $argument) { - return new TaggedIteratorArgument($argument); + return new TaggedIteratorArgument($argument, null, null, $forLocator); } if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) { if ($diff = array_diff(array_keys($argument), ['tag', 'index_by', 'default_index_method'])) { - throw new InvalidArgumentException(sprintf('"!tagged" tag contains unsupported key "%s"; supported ones are "tag", "index_by" and "default_index_method".', implode('"", "', $diff))); + throw new InvalidArgumentException(sprintf('"!%s" tag contains unsupported key "%s"; supported ones are "tag", "index_by" and "default_index_method".', $value->getTag(), implode('"", "', $diff))); } - return new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null); - } + $argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'], $argument['default_index_method'] ?? null, $forLocator); - throw new InvalidArgumentException(sprintf('"!tagged" tags only accept a non empty string or an array with a key "tag" in "%s".', $file)); - } - if ('tagged_locator' === $value->getTag()) { - if (\is_array($argument) && isset($argument['tag'], $argument['index_by']) && $argument['tag'] && $argument['index_by']) { - if ($diff = array_diff(array_keys($argument), ['tag', 'index_by', 'default_index_method'])) { - throw new InvalidArgumentException(sprintf('"!tagged_locator" tag contains unsupported key "%s"; supported ones are "tag", "index_by" and "default_index_method".', implode('"", "', $diff))); + if ($forLocator) { + $argument = new ServiceLocatorArgument($argument); } - return new ServiceLocatorArgument(new TaggedIteratorArgument($argument['tag'], $argument['index_by'], $argument['default_index_method'] ?? null)); + return $argument; } - throw new InvalidArgumentException(sprintf('"!tagged_locator" tags only accept an array with at least keys "tag" and "index_by" in "%s".', $file)); + throw new InvalidArgumentException(sprintf('"!%s" tags only accept a non empty string or an array with a key "tag" in "%s".', $value->getTag(), $file)); } if ('service' === $value->getTag()) { if ($isParameter) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index a2bbbe20cf..feeb163edb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -350,6 +350,32 @@ class IntegrationTest extends TestCase ]; $this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $same); } + + public function testTaggedServiceLocatorWithFqcnFallback() + { + $container = new ContainerBuilder(); + $container->register(BarTagClass::class) + ->setPublic(true) + ->addTag('foo_bar') + ; + $container->register(FooBarTaggedClass::class) + ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', null, null, true))) + ->setPublic(true) + ; + + $container->compile(); + + $s = $container->get(FooBarTaggedClass::class); + + /** @var ServiceLocator $serviceLocator */ + $serviceLocator = $s->getParam(); + $this->assertTrue($s->getParam() instanceof ServiceLocator, sprintf('Wrong instance, should be an instance of ServiceLocator, %s given', \is_object($serviceLocator) ? \get_class($serviceLocator) : \gettype($serviceLocator))); + + $same = [ + BarTagClass::class => $serviceLocator->get(BarTagClass::class), + ]; + $this->assertSame([BarTagClass::class => $container->get(BarTagClass::class)], $same); + } } class ServiceSubscriberStub implements ServiceSubscriberInterface diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php index adf9696acd..57ca9af427 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php @@ -93,6 +93,6 @@ class PriorityTaggedServiceTraitImplementation public function test($tagName, ContainerBuilder $container) { - return self::findAndSortTaggedServices($tagName, $container); + return $this->findAndSortTaggedServices($tagName, $container); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 3e326ed85a..681a43b332 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -329,6 +329,8 @@ class XmlFileLoaderTest extends TestCase $taggedIterator = new TaggedIteratorArgument('foo_tag', 'barfoo', 'foobar'); $this->assertEquals($taggedIterator, $container->getDefinition('foo_tagged_iterator')->getArgument(0)); + + $taggedIterator = new TaggedIteratorArgument('foo_tag', 'barfoo', 'foobar', true); $this->assertEquals(new ServiceLocatorArgument($taggedIterator), $container->getDefinition('foo_tagged_locator')->getArgument(0)); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 0da99f437b..83e034c482 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -293,6 +293,8 @@ class YamlFileLoaderTest extends TestCase $taggedIterator = new TaggedIteratorArgument('foo', 'barfoo', 'foobar'); $this->assertEquals($taggedIterator, $container->getDefinition('foo_service_tagged_iterator')->getArgument(0)); + + $taggedIterator = new TaggedIteratorArgument('foo', 'barfoo', 'foobar', true); $this->assertEquals(new ServiceLocatorArgument($taggedIterator), $container->getDefinition('foo_service_tagged_locator')->getArgument(0)); }