[DependencyInjection] Add #[TaggedItem] attribute for defining the index and priority of classes found in tagged iterators/locators

This commit is contained in:
Nicolas Grekas 2021-02-18 22:51:00 +01:00
parent 59e5ac5dcf
commit 252f2ca1fb
4 changed files with 96 additions and 39 deletions

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Attribute;
/**
* An attribute to tell under which index and priority a service class should be found in tagged iterators/locators.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class TaggedItem
{
public function __construct(
public string $index,
public ?int $priority = null,
) {
}
}

View File

@ -7,6 +7,7 @@ CHANGELOG
* Add `ServicesConfigurator::remove()` in the PHP-DSL * Add `ServicesConfigurator::remove()` in the PHP-DSL
* Add `%env(not:...)%` processor to negate boolean values * Add `%env(not:...)%` processor to negate boolean values
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8 * Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
* Add `#[TaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators
* Add autoconfigurable attributes * Add autoconfigurable attributes
* Add support for per-env configuration in loaders * Add support for per-env configuration in loaders
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration * Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\TaggedItem;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
@ -56,8 +57,10 @@ trait PriorityTaggedServiceTrait
foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) { foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
$defaultPriority = null; $defaultPriority = null;
$defaultIndex = null; $defaultIndex = null;
$class = $container->getDefinition($serviceId)->getClass(); $definition = $container->getDefinition($serviceId);
$class = $definition->getClass();
$class = $container->getParameterBag()->resolveValue($class) ?: null; $class = $container->getParameterBag()->resolveValue($class) ?: null;
$checkTaggedItem = !$definition->hasTag(80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() ? 'container.ignore_attributes' : $tagName);
foreach ($attributes as $attribute) { foreach ($attributes as $attribute) {
$index = $priority = null; $index = $priority = null;
@ -65,7 +68,7 @@ trait PriorityTaggedServiceTrait
if (isset($attribute['priority'])) { if (isset($attribute['priority'])) {
$priority = $attribute['priority']; $priority = $attribute['priority'];
} elseif (null === $defaultPriority && $defaultPriorityMethod && $class) { } elseif (null === $defaultPriority && $defaultPriorityMethod && $class) {
$defaultPriority = PriorityTaggedServiceUtil::getDefaultPriority($container, $serviceId, $class, $defaultPriorityMethod, $tagName); $defaultPriority = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem);
} }
$priority = $priority ?? $defaultPriority ?? $defaultPriority = 0; $priority = $priority ?? $defaultPriority ?? $defaultPriority = 0;
@ -77,7 +80,7 @@ trait PriorityTaggedServiceTrait
if (null !== $indexAttribute && isset($attribute[$indexAttribute])) { if (null !== $indexAttribute && isset($attribute[$indexAttribute])) {
$index = $attribute[$indexAttribute]; $index = $attribute[$indexAttribute];
} elseif (null === $defaultIndex && $defaultPriorityMethod && $class) { } elseif (null === $defaultIndex && $defaultPriorityMethod && $class) {
$defaultIndex = PriorityTaggedServiceUtil::getDefaultIndex($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute); $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
} }
$index = $index ?? $defaultIndex ?? $defaultIndex = $serviceId; $index = $index ?? $defaultIndex ?? $defaultIndex = $serviceId;
@ -114,22 +117,30 @@ trait PriorityTaggedServiceTrait
class PriorityTaggedServiceUtil class PriorityTaggedServiceUtil
{ {
/** /**
* Gets the index defined by the default index method. * @return string|int|null
*/ */
public static function getDefaultIndex(ContainerBuilder $container, string $serviceId, string $class, string $defaultIndexMethod, string $tagName, ?string $indexAttribute): ?string public static function getDefault(ContainerBuilder $container, string $serviceId, string $class, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem)
{ {
if (!($r = $container->getReflectionClass($class)) || !$r->hasMethod($defaultIndexMethod)) { if (!($r = $container->getReflectionClass($class)) || (!$checkTaggedItem && !$r->hasMethod($defaultMethod))) {
return null;
}
if ($checkTaggedItem && !$r->hasMethod($defaultMethod)) {
foreach ($r->getAttributes(TaggedItem::class) as $attribute) {
return 'priority' === $indexAttribute ? $attribute->newInstance()->priority : $attribute->newInstance()->index;
}
return null; return null;
} }
if (null !== $indexAttribute) { if (null !== $indexAttribute) {
$service = $class !== $serviceId ? sprintf('service "%s"', $serviceId) : 'on the corresponding service'; $service = $class !== $serviceId ? sprintf('service "%s"', $serviceId) : 'on the corresponding service';
$message = [sprintf('Either method "%s::%s()" should ', $class, $defaultIndexMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)]; $message = [sprintf('Either method "%s::%s()" should ', $class, $defaultMethod), sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)];
} else { } else {
$message = [sprintf('Method "%s::%s()" should ', $class, $defaultIndexMethod), '.']; $message = [sprintf('Method "%s::%s()" should ', $class, $defaultMethod), '.'];
} }
if (!($rm = $r->getMethod($defaultIndexMethod))->isStatic()) { if (!($rm = $r->getMethod($defaultMethod))->isStatic()) {
throw new InvalidArgumentException(implode('be static', $message)); throw new InvalidArgumentException(implode('be static', $message));
} }
@ -137,42 +148,24 @@ class PriorityTaggedServiceUtil
throw new InvalidArgumentException(implode('be public', $message)); throw new InvalidArgumentException(implode('be public', $message));
} }
$defaultIndex = $rm->invoke(null); $default = $rm->invoke(null);
if (\is_int($defaultIndex)) { if ('priority' === $indexAttribute) {
$defaultIndex = (string) $defaultIndex; if (!\is_int($default)) {
throw new InvalidArgumentException(implode(sprintf('return int (got "%s")', get_debug_type($default)), $message));
}
return $default;
} }
if (!\is_string($defaultIndex)) { if (\is_int($default)) {
throw new InvalidArgumentException(implode(sprintf('return string|int (got "%s")', get_debug_type($defaultIndex)), $message)); $default = (string) $default;
} }
return $defaultIndex; if (!\is_string($default)) {
} throw new InvalidArgumentException(implode(sprintf('return string|int (got "%s")', get_debug_type($default)), $message));
/**
* Gets the priority defined by the default priority method.
*/
public static function getDefaultPriority(ContainerBuilder $container, string $serviceId, string $class, string $defaultPriorityMethod, string $tagName): ?int
{
if (!($r = $container->getReflectionClass($class)) || !$r->hasMethod($defaultPriorityMethod)) {
return null;
} }
if (!($rm = $r->getMethod($defaultPriorityMethod))->isStatic()) { return $default;
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));
}
if (!$rm->isPublic()) {
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));
}
$defaultPriority = $rm->invoke(null);
if (!\is_int($defaultPriority)) {
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));
}
return $defaultPriority;
} }
} }

View File

@ -13,7 +13,10 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\TaggedItem;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
@ -188,6 +191,34 @@ class PriorityTaggedServiceTraitTest extends TestCase
yield ['getMethodShouldBePublicInsteadPrivate', null, sprintf('Method "%s::getMethodShouldBePublicInsteadPrivate()" should be public.', FooTaggedForInvalidDefaultMethodClass::class)]; yield ['getMethodShouldBePublicInsteadPrivate', null, sprintf('Method "%s::getMethodShouldBePublicInsteadPrivate()" should be public.', FooTaggedForInvalidDefaultMethodClass::class)];
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)]; 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)];
} }
/**
* @requires PHP 8
*/
public function testTaggedItemAttributes()
{
$container = new ContainerBuilder();
$container->register('service1', FooTagClass::class)->addTag('my_custom_tag');
$container->register('service2', HelloNamedService::class)
->setAutoconfigured(true)
->setInstanceofConditionals([
HelloNamedService::class => (new ChildDefinition(''))->addTag('my_custom_tag'),
\stdClass::class => (new ChildDefinition(''))->addTag('my_custom_tag2'),
]);
(new ResolveInstanceofConditionalsPass())->process($container);
$priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation();
$tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar');
$expected = [
'hello' => new TypedReference('service2', HelloNamedService::class),
'service1' => new TypedReference('service1', FooTagClass::class),
];
$services = $priorityTaggedServiceTraitImplementation->test($tag, $container);
$this->assertSame(array_keys($expected), array_keys($services));
$this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container));
}
} }
class PriorityTaggedServiceTraitImplementation class PriorityTaggedServiceTraitImplementation
@ -199,3 +230,8 @@ class PriorityTaggedServiceTraitImplementation
return $this->findAndSortTaggedServices($tagName, $container); return $this->findAndSortTaggedServices($tagName, $container);
} }
} }
#[TaggedItem(index: 'hello', priority: 1)]
class HelloNamedService extends \stdClass
{
}