Support indexing tagged locators by FQCN as fallback

This commit is contained in:
Nicolas Grekas 2019-03-07 15:19:37 +01:00 committed by Anthony MARTIN
parent 250a2c8332
commit cb3c56bc0c
11 changed files with 108 additions and 79 deletions

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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)));
}

View File

@ -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));
}
/**

View File

@ -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)) {

View File

@ -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) {

View File

@ -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

View File

@ -93,6 +93,6 @@ class PriorityTaggedServiceTraitImplementation
public function test($tagName, ContainerBuilder $container)
{
return self::findAndSortTaggedServices($tagName, $container);
return $this->findAndSortTaggedServices($tagName, $container);
}
}

View File

@ -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));
}

View File

@ -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));
}