From 6f32584c7663a1303580ca06a81dcc5a478fff59 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Fri, 4 Oct 2019 16:41:42 +0200 Subject: [PATCH] [EventDispatcher] Allow to omit the event name when registering listeners. --- .../Component/EventDispatcher/CHANGELOG.md | 1 + .../RegisterListenersPass.php | 28 +++- .../RegisterListenersPassTest.php | 151 ++++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/EventDispatcher/CHANGELOG.md b/src/Symfony/Component/EventDispatcher/CHANGELOG.md index d607b808a0..2e5afbc6f5 100644 --- a/src/Symfony/Component/EventDispatcher/CHANGELOG.md +++ b/src/Symfony/Component/EventDispatcher/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`. +* Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events. 4.3.0 ----- diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index ffded127ac..9c88809de0 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -16,8 +16,10 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\Event as LegacyEvent; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\EventDispatcher\Event; /** * Compiler pass to register tagged services for an event dispatcher. @@ -67,8 +69,14 @@ class RegisterListenersPass implements CompilerPassInterface $priority = isset($event['priority']) ? $event['priority'] : 0; if (!isset($event['event'])) { - throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + if ($container->getDefinition($id)->hasTag($this->subscriberTag)) { + continue; + } + + $event['method'] = $event['method'] ?? '__invoke'; + $event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']); } + $event['event'] = $aliases[$event['event']] ?? $event['event']; if (!isset($event['method'])) { @@ -122,6 +130,24 @@ class RegisterListenersPass implements CompilerPassInterface ExtractingEventDispatcher::$aliases = []; } } + + private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string + { + if ( + null === ($class = $container->getDefinition($id)->getClass()) + || !($r = $container->getReflectionClass($class, false)) + || !$r->hasMethod($method) + || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() + || !($type = $m->getParameters()[0]->getType()) + || $type->isBuiltin() + || Event::class === ($name = $type->getName()) + || LegacyEvent::class === $name + ) { + throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + } + + return $name; + } } /** diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index c21d6ca1cf..5252664a9f 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; @@ -244,6 +245,116 @@ class RegisterListenersPassTest extends TestCase ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } + + public function testOmitEventNameOnTypedListener(): void + { + $container = new ContainerBuilder(); + $container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']); + $container->register('foo', TypedListener::class)->addTag('kernel.event_listener', ['method' => 'onEvent']); + $container->register('bar', TypedListener::class)->addTag('kernel.event_listener'); + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + + $definition = $container->getDefinition('event_dispatcher'); + $expectedCalls = [ + [ + 'addListener', + [ + CustomEvent::class, + [new ServiceClosureArgument(new Reference('foo')), 'onEvent'], + 0, + ], + ], + [ + 'addListener', + [ + 'aliased_event', + [new ServiceClosureArgument(new Reference('bar')), '__invoke'], + 0, + ], + ], + ]; + $this->assertEquals($expectedCalls, $definition->getMethodCalls()); + } + + public function testOmitEventNameOnUntypedListener(): void + { + $container = new ContainerBuilder(); + $container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener', ['method' => 'onEvent']); + $container->register('event_dispatcher'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + } + + public function testOmitEventNameAndMethodOnUntypedListener(): void + { + $container = new ContainerBuilder(); + $container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener'); + $container->register('event_dispatcher'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + } + + /** + * @requires PHP 7.2 + */ + public function testOmitEventNameAndMethodOnGenericListener(): void + { + $container = new ContainerBuilder(); + $container->register('foo', GenericListener::class)->addTag('kernel.event_listener'); + $container->register('event_dispatcher'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + } + + public function testOmitEventNameOnSubscriber(): void + { + $container = new ContainerBuilder(); + $container->register('subscriber', IncompleteSubscriber::class) + ->addTag('kernel.event_subscriber') + ->addTag('kernel.event_listener') + ->addTag('kernel.event_listener', ['event' => 'bar', 'method' => 'onBar']) + ; + $container->register('event_dispatcher'); + + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->process($container); + + $definition = $container->getDefinition('event_dispatcher'); + $expectedCalls = [ + [ + 'addListener', + [ + 'bar', + [new ServiceClosureArgument(new Reference('subscriber')), 'onBar'], + 0, + ], + ], + [ + 'addListener', + [ + 'foo', + [new ServiceClosureArgument(new Reference('subscriber')), 'onFoo'], + 0, + ], + ], + ]; + $this->assertEquals($expectedCalls, $definition->getMethodCalls()); + } } class SubscriberService implements EventSubscriberInterface @@ -285,3 +396,43 @@ final class AliasedEvent final class CustomEvent { } + +final class TypedListener +{ + public function __invoke(AliasedEvent $event): void + { + } + + public function onEvent(CustomEvent $event): void + { + } +} + +final class GenericListener +{ + public function __invoke(object $event): void + { + } +} + +final class IncompleteSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'foo' => 'onFoo', + ]; + } + + public function onFoo(): void + { + } + + public function onBar(): void + { + } + + public function __invoke(CustomEvent $event): void + { + } +}