From 917fcc09f762f95f7fb9354b0ce80e11a3139bec Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 9 Jul 2021 12:48:53 +0200 Subject: [PATCH] [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters --- .../FrameworkExtension.php | 11 +- .../DependencyInjection/CHANGELOG.md | 3 +- .../AttributeAutoconfigurationPass.php | 117 ++++++++++++++++-- .../DependencyInjection/ContainerBuilder.php | 10 +- .../AttributeAutoconfigurationPassTest.php | 15 +++ .../Tests/Compiler/IntegrationTest.php | 85 +++++++++++++ .../Fixtures/Attribute/CustomAnyAttribute.php | 17 +++ .../Attribute/CustomMethodAttribute.php | 22 ++++ .../Attribute/CustomParameterAttribute.php | 22 ++++ .../Attribute/CustomPropertyAttribute.php | 22 ++++ .../Tests/Fixtures/TaggedService4.php | 48 +++++++ .../Attribute/AsEventListener.php | 2 +- .../Component/EventDispatcher/CHANGELOG.md | 5 + .../RegisterListenersPassTest.php | 22 +++- .../Tests/Fixtures/TaggedMultiListener.php | 5 + 15 files changed, 390 insertions(+), 16 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomAnyAttribute.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomMethodAttribute.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomParameterAttribute.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomPropertyAttribute.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TaggedService4.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 44c37e270946a..1f0e198924aea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -555,8 +555,15 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(LoggerAwareInterface::class) ->addMethodCall('setLogger', [new Reference('logger')]); - $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute): void { - $definition->addTag('kernel.event_listener', get_object_vars($attribute)); + $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \Reflector $reflector) { + $tagAttributes = get_object_vars($attribute); + if ($reflector instanceof \ReflectionMethod) { + if (isset($tagAttributes['method'])) { + throw new LogicException(sprintf('AsEventListener attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); + } + $tagAttributes['method'] = $reflector->getName(); + } + $definition->addTag('kernel.event_listener', $tagAttributes); }); $container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void { $definition->addTag('controller.service_arguments'); diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 412d4388de2f1..d2fa12a70e7db 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,7 +4,8 @@ CHANGELOG 5.4 --- -* Add `service_closure()` to the PHP-DSL + * Add `service_closure()` to the PHP-DSL + * Add support for autoconfigurable attributes on methods, properties and parameters 5.3 --- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php index ade7eaba39d71..f5094996d949f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php @@ -14,18 +14,66 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; /** * @author Alexander M. Turek */ final class AttributeAutoconfigurationPass extends AbstractRecursivePass { + private $classAttributeConfigurators = []; + private $methodAttributeConfigurators = []; + private $propertyAttributeConfigurators = []; + private $parameterAttributeConfigurators = []; + public function process(ContainerBuilder $container): void { if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) { return; } + foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) { + $callableReflector = new \ReflectionFunction(\Closure::fromCallable($callable)); + if ($callableReflector->getNumberOfParameters() <= 2) { + $this->classAttributeConfigurators[$attributeName] = $callable; + continue; + } + + $reflectorParameter = $callableReflector->getParameters()[2]; + $parameterType = $reflectorParameter->getType(); + $types = []; + if ($parameterType instanceof \ReflectionUnionType) { + foreach ($parameterType->getTypes() as $type) { + $types[] = $type->getName(); + } + } elseif ($parameterType instanceof \ReflectionNamedType) { + $types[] = $parameterType->getName(); + } else { + throw new LogicException(sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine())); + } + + try { + $attributeReflector = new \ReflectionClass($attributeName); + } catch (\ReflectionException $e) { + continue; + } + + $targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0; + $targets = $targets ? $targets->getArguments()[0] ?? -1 : 0; + + foreach (['class', 'method', 'property', 'parameter'] as $symbol) { + if (['Reflector'] !== $types) { + if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) { + continue; + } + if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) { + throw new LogicException(sprintf('Invalid type "Reflection%s" on argument "$%s": attribute "%s" cannot target a '.$symbol.' in "%s" on line "%d".', ucfirst($symbol), $reflectorParameter->getName(), $attributeName, $callableReflector->getFileName(), $callableReflector->getStartLine())); + } + } + $this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable; + } + } + parent::process($container); } @@ -35,21 +83,74 @@ protected function processValue($value, bool $isRoot = false) || !$value->isAutoconfigured() || $value->isAbstract() || $value->hasTag('container.ignore_attributes') - || !($reflector = $this->container->getReflectionClass($value->getClass(), false)) + || !($classReflector = $this->container->getReflectionClass($value->getClass(), false)) ) { return parent::processValue($value, $isRoot); } - $autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes(); $instanceof = $value->getInstanceofConditionals(); - $conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition(''); - foreach ($reflector->getAttributes() as $attribute) { - if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) { - $configurator($conditionals, $attribute->newInstance(), $reflector); + $conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition(''); + + if ($this->classAttributeConfigurators) { + foreach ($classReflector->getAttributes() as $attribute) { + if ($configurator = $this->classAttributeConfigurators[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $classReflector); + } } } - if (!isset($instanceof[$reflector->getName()]) && new ChildDefinition('') != $conditionals) { - $instanceof[$reflector->getName()] = $conditionals; + + if ($this->parameterAttributeConfigurators && $constructorReflector = $this->getConstructor($value, false)) { + foreach ($constructorReflector->getParameters() as $parameterReflector) { + foreach ($parameterReflector->getAttributes() as $attribute) { + if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $parameterReflector); + } + } + } + } + + if ($this->methodAttributeConfigurators || $this->parameterAttributeConfigurators) { + foreach ($classReflector->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflector) { + if ($methodReflector->isStatic() || $methodReflector->isConstructor() || $methodReflector->isDestructor()) { + continue; + } + + if ($this->methodAttributeConfigurators) { + foreach ($methodReflector->getAttributes() as $attribute) { + if ($configurator = $this->methodAttributeConfigurators[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $methodReflector); + } + } + } + + if ($this->parameterAttributeConfigurators) { + foreach ($methodReflector->getParameters() as $parameterReflector) { + foreach ($parameterReflector->getAttributes() as $attribute) { + if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $parameterReflector); + } + } + } + } + } + } + + if ($this->propertyAttributeConfigurators) { + foreach ($classReflector->getProperties(\ReflectionProperty::IS_PUBLIC) as $propertyReflector) { + if ($propertyReflector->isStatic()) { + continue; + } + + foreach ($propertyReflector->getAttributes() as $attribute) { + if ($configurator = $this->propertyAttributeConfigurators[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $propertyReflector); + } + } + } + } + + if (!isset($instanceof[$classReflector->getName()]) && new ChildDefinition('') != $conditionals) { + $instanceof[$classReflector->getName()] = $conditionals; $value->setInstanceofConditionals($instanceof); } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 292c2e19e70f4..066c9ad2fc390 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1309,7 +1309,15 @@ public function registerForAutoconfiguration(string $interface) /** * Registers an attribute that will be used for autoconfiguring annotated classes. * - * The configurator will receive a ChildDefinition instance, an instance of the attribute and the corresponding \ReflectionClass, in that order. + * The third argument passed to the callable is the reflector of the + * class/method/property/parameter that the attribute targets. Using one or many of + * \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter as a type-hint + * for this argument allows filtering which attributes should be passed to the callable. + * + * @template T + * + * @param class-string $attributeClass + * @param callable(ChildDefinition, T, \Reflector): void $configurator */ public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php index 7b14c28133d9a..aca5f52b9e680 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AttributeAutoconfigurationPassTest.php @@ -13,8 +13,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; /** * @requires PHP 8 @@ -33,4 +35,17 @@ public function testProcessAddsNoEmptyInstanceofConditionals() $this->assertSame([], $container->getDefinition('foo')->getInstanceofConditionals()); } + + public function testAttributeConfiguratorCallableMissingType() + { + $container = new ContainerBuilder(); + $container->registerAttributeForAutoconfiguration(AsTaggedItem::class, static function (ChildDefinition $definition, AsTaggedItem $attribute, $reflector) {}); + $container->register('foo', \stdClass::class) + ->setAutoconfigured(true) + ; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Argument "$reflector" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in '); + (new AttributeAutoconfigurationPass())->process($container); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index 848bb7445e10a..ceb199ac81990 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -23,7 +23,11 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute; use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAutoconfiguration; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomMethodAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomParameterAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomPropertyAttribute; use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass; @@ -37,6 +41,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3Configurator; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService4; use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -729,6 +734,86 @@ static function (Definition $definition, CustomAutoconfiguration $attribute) { ], $collector->collectedTags); } + /** + * @requires PHP 8 + */ + public function testTagsViaAttributeOnPropertyMethodAndParameter() + { + $container = new ContainerBuilder(); + $container->registerAttributeForAutoconfiguration( + CustomMethodAttribute::class, + static function (ChildDefinition $definition, CustomMethodAttribute $attribute, \ReflectionMethod $reflector) { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['method'] = $reflector->getName(); + + $definition->addTag('app.custom_tag', $tagAttributes); + } + ); + $container->registerAttributeForAutoconfiguration( + CustomPropertyAttribute::class, + static function (ChildDefinition $definition, CustomPropertyAttribute $attribute, \ReflectionProperty $reflector) { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['property'] = $reflector->getName(); + + $definition->addTag('app.custom_tag', $tagAttributes); + } + ); + $container->registerAttributeForAutoconfiguration( + CustomParameterAttribute::class, + static function (ChildDefinition $definition, CustomParameterAttribute $attribute, \ReflectionParameter $reflector) { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['parameter'] = $reflector->getName(); + + $definition->addTag('app.custom_tag', $tagAttributes); + } + ); + $container->registerAttributeForAutoconfiguration( + CustomAnyAttribute::class, + eval(<<<'PHP' + return static function (\Symfony\Component\DependencyInjection\ChildDefinition $definition, \Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $reflector) { + $tagAttributes = get_object_vars($attribute); + if ($reflector instanceof \ReflectionClass) { + $tagAttributes['class'] = $reflector->getName(); + } elseif ($reflector instanceof \ReflectionMethod) { + $tagAttributes['method'] = $reflector->getName(); + } elseif ($reflector instanceof \ReflectionProperty) { + $tagAttributes['property'] = $reflector->getName(); + } elseif ($reflector instanceof \ReflectionParameter) { + $tagAttributes['parameter'] = $reflector->getName(); + } + + $definition->addTag('app.custom_tag', $tagAttributes); + }; +PHP + )); + + $container->register(TaggedService4::class) + ->setPublic(true) + ->setAutoconfigured(true); + + $collector = new TagCollector(); + $container->addCompilerPass($collector); + + $container->compile(); + + self::assertSame([ + TaggedService4::class => [ + ['class' => TaggedService4::class], + ['parameter' => 'param1'], + ['someAttribute' => 'on param1 in constructor', 'priority' => 0, 'parameter' => 'param1'], + ['parameter' => 'param2'], + ['someAttribute' => 'on param2 in constructor', 'priority' => 0, 'parameter' => 'param2'], + ['method' => 'fooAction'], + ['someAttribute' => 'on fooAction', 'priority' => 0, 'method' => 'fooAction'], + ['someAttribute' => 'on param1 in fooAction', 'priority' => 0, 'parameter' => 'param1'], + ['method' => 'barAction'], + ['someAttribute' => 'on barAction', 'priority' => 0, 'method' => 'barAction'], + ['property' => 'name'], + ['someAttribute' => 'on name', 'priority' => 0, 'property' => 'name'], + ], + ], $collector->collectedTags); + } + /** * @requires PHP 8 */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomAnyAttribute.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomAnyAttribute.php new file mode 100644 index 0000000000000..c9c59cb519b19 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomAnyAttribute.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)] +final class CustomAnyAttribute +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomMethodAttribute.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomMethodAttribute.php new file mode 100644 index 0000000000000..aae42bb26daf1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomMethodAttribute.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute; + +#[\Attribute(\Attribute::TARGET_METHOD)] +final class CustomMethodAttribute +{ + public function __construct( + public string $someAttribute, + public int $priority = 0, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomParameterAttribute.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomParameterAttribute.php new file mode 100644 index 0000000000000..2fcb52c229289 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomParameterAttribute.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +final class CustomParameterAttribute +{ + public function __construct( + public string $someAttribute, + public int $priority = 0, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomPropertyAttribute.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomPropertyAttribute.php new file mode 100644 index 0000000000000..3b236988db941 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Attribute/CustomPropertyAttribute.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute; + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +final class CustomPropertyAttribute +{ + public function __construct( + public string $someAttribute, + public int $priority = 0, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TaggedService4.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TaggedService4.php new file mode 100644 index 0000000000000..87830e59bde4d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TaggedService4.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomMethodAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomPropertyAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomParameterAttribute; + +#[CustomAnyAttribute] +final class TaggedService4 +{ + #[CustomAnyAttribute] + #[CustomPropertyAttribute(someAttribute: "on name")] + public string $name; + + public function __construct( + #[CustomAnyAttribute] + #[CustomParameterAttribute(someAttribute: "on param1 in constructor")] + private string $param1, + #[CustomAnyAttribute] + #[CustomParameterAttribute(someAttribute: "on param2 in constructor")] + string $param2 + ) {} + + #[CustomAnyAttribute] + #[CustomMethodAttribute(someAttribute: "on fooAction")] + public function fooAction( + #[CustomAnyAttribute] + #[CustomParameterAttribute(someAttribute: "on param1 in fooAction")] + string $param1 + ) {} + + #[CustomAnyAttribute] + #[CustomMethodAttribute(someAttribute: "on barAction")] + public function barAction() {} + + public function someOtherMethod() {} +} diff --git a/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php b/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php index f42d4bddd2556..bb931b82dc2b1 100644 --- a/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php +++ b/src/Symfony/Component/EventDispatcher/Attribute/AsEventListener.php @@ -16,7 +16,7 @@ * * @author Alexander M. Turek */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class AsEventListener { public function __construct( diff --git a/src/Symfony/Component/EventDispatcher/CHANGELOG.md b/src/Symfony/Component/EventDispatcher/CHANGELOG.md index 4172876304155..0f985989502a6 100644 --- a/src/Symfony/Component/EventDispatcher/CHANGELOG.md +++ b/src/Symfony/Component/EventDispatcher/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.4 +--- + + * Allow `#[AsEventListener]` attribute on methods + 5.3 --- diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index 36287b893755b..5a965c058227a 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -291,9 +291,17 @@ public function testTaggedMultiEventListener() } $container = new ContainerBuilder(); - $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute): void { - $definition->addTag('kernel.event_listener', get_object_vars($attribute)); - }); + $container->registerAttributeForAutoconfiguration(AsEventListener::class, eval(<<<'PHP' + return static function (\Symfony\Component\DependencyInjection\ChildDefinition $definition, \Symfony\Component\EventDispatcher\Attribute\AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector): void { + $tagAttributes = get_object_vars($attribute); + if ($reflector instanceof \ReflectionMethod) { + $tagAttributes['method'] = $reflector->getName(); + } + $definition->addTag('kernel.event_listener', $tagAttributes); + }; +PHP + )); + $container->register('foo', TaggedMultiListener::class)->setAutoconfigured(true); $container->register('event_dispatcher', \stdClass::class); @@ -327,6 +335,14 @@ public function testTaggedMultiEventListener() 0, ], ], + [ + 'addListener', + [ + 'baz', + [new ServiceClosureArgument(new Reference('foo')), 'onBazEvent'], + 0, + ], + ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } diff --git a/src/Symfony/Component/EventDispatcher/Tests/Fixtures/TaggedMultiListener.php b/src/Symfony/Component/EventDispatcher/Tests/Fixtures/TaggedMultiListener.php index 7e307162cbcc2..db7f463c5509e 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/Fixtures/TaggedMultiListener.php +++ b/src/Symfony/Component/EventDispatcher/Tests/Fixtures/TaggedMultiListener.php @@ -29,4 +29,9 @@ public function onFoo(): void public function onBarEvent(): void { } + + #[AsEventListener(event: 'baz')] + public function onBazEvent(): void + { + } }