From 359870e6a61040a4a7e739bb0ded1e7bb956af8d Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sun, 31 Mar 2019 18:06:22 +0200 Subject: [PATCH] Infer events a subscriber subscribes to from listener parameters. --- .../RegisterListenersPass.php | 5 + .../EventDispatcher/EventDispatcher.php | 41 +++++++ .../EventSubscriberInterface.php | 6 + .../RegisterListenersPassTest.php | 67 ++++++++++- .../Tests/EventDispatcherTest.php | 106 ++++++++++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 3806990f35fa7..7f077d913ed3b 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -149,4 +149,9 @@ public static function getSubscribedEvents() return $events; } + + protected function inferEventName($subscriber, $params): string + { + return parent::inferEventName($subscriber instanceof self ? self::$subscriber : $subscriber, $params); + } } diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index 60283882a8848..828e59e6d5460 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -190,6 +190,9 @@ public function removeListener($eventName, $listener) public function addSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_int($eventName)) { + $eventName = $this->inferEventName($subscriber, $params); + } if (\is_string($params)) { $this->addListener($eventName, [$subscriber, $params]); } elseif (\is_string($params[0])) { @@ -208,6 +211,9 @@ public function addSubscriber(EventSubscriberInterface $subscriber) public function removeSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_int($eventName)) { + $eventName = $this->inferEventName($subscriber, $params); + } if (\is_array($params) && \is_array($params[0])) { foreach ($params as $listener) { $this->removeListener($eventName, [$subscriber, $listener[0]]); @@ -259,6 +265,41 @@ protected function doDispatch($listeners, $eventName, Event $event) } } + /** + * @param string|object $subscriber + * @param string|array $params + * + * @return string + */ + protected function inferEventName($subscriber, $params): string + { + $methodName = \is_array($params) ? $params[0] : $params; + + try { + $parameters = (new \ReflectionMethod($subscriber, $methodName))->getParameters(); + } catch (\ReflectionException $e) { + throw new \UnexpectedValueException(sprintf( + 'Cannot infer event name for missing method "%s::%s".', + \is_object($subscriber) ? \get_class($subscriber) : $subscriber, + $methodName + ), 0, $e); + } + + if ( + empty($parameters) + || null === ($type = $parameters[0]->getType()) + || $type->isBuiltin() + ) { + throw new \UnexpectedValueException(sprintf( + 'Cannot infer event name for method "%s::%s". Please add a type-hint for the event calls to the first parameter of the method or configure the event name explicitly.', + \is_object($subscriber) ? \get_class($subscriber) : $subscriber, + $methodName + )); + } + + return $type->getName(); + } + /** * Sorts the internal list of listeners for the given event by priority. */ diff --git a/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php b/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php index 824f21599c256..f276942404cb3 100644 --- a/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php +++ b/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php @@ -40,6 +40,12 @@ interface EventSubscriberInterface * * ['eventName' => ['methodName', $priority]] * * ['eventName' => [['methodName1', $priority], ['methodName2']]] * + * Alternatively, the keys can be omitted. In that case, the event name is + * inferred from the type hint of the first parameter of the specified method. + * + * * ['methodName'] + * * [['methodName1', $priority], 'methodName2'] + * * @return array The event names to listen to */ public static function getSubscribedEvents(); diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index d665f426e0236..ccf03864d75af 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -16,6 +16,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\EventDispatcher\Event; class RegisterListenersPassTest extends TestCase { @@ -59,6 +61,30 @@ public function testValidEventSubscriber() 0, ], ], + [ + 'addListener', + [ + MockEvent::class, + [new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onExplicitlyConfiguredEvent'], + 0, + ], + ], + [ + 'addListener', + [ + MockEvent::class, + [new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onMockEvent'], + 0, + ], + ], + [ + 'addListener', + [ + MockEvent::class, + [new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onEarlyMockEvent'], + 255, + ], + ], ]; $this->assertEquals($expectedCalls, $eventDispatcherDefinition->getMethodCalls()); } @@ -112,6 +138,30 @@ public function testEventSubscriberResolvableClassName() 0, ], ], + [ + 'addListener', + [ + MockEvent::class, + [new ServiceClosureArgument(new Reference('foo')), 'onExplicitlyConfiguredEvent'], + 0, + ], + ], + [ + 'addListener', + [ + MockEvent::class, + [new ServiceClosureArgument(new Reference('foo')), 'onMockEvent'], + 0, + ], + ], + [ + 'addListener', + [ + MockEvent::class, + [new ServiceClosureArgument(new Reference('foo')), 'onEarlyMockEvent'], + 255, + ], + ], ]; $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } @@ -184,14 +234,29 @@ public function testInvokableEventListener() } } -class SubscriberService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface +class SubscriberService implements EventSubscriberInterface { public static function getSubscribedEvents() { return [ 'event' => 'onEvent', + MockEvent::class => 'onExplicitlyConfiguredEvent', + 'onMockEvent', + ['onEarlyMockEvent', 255], ]; } + + public function onEarlyMockEvent(MockEvent $event): void + { + } + + public function onMockEvent(MockEvent $event): void + { + } +} + +class MockEvent extends Event +{ } class InvokableListenerService diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php index e89f78cda8e89..1c38904a635a2 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php @@ -243,6 +243,49 @@ public function testAddSubscriberWithMultipleListeners() $this->assertEquals('preFoo2', $listeners[0][1]); } + public function testAddSubscriberWithoutEventName() + { + $eventSubscriber = new TestEventSubscriberWithoutEventName(); + $this->dispatcher->addSubscriber($eventSubscriber); + $this->assertCount(2, $this->dispatcher->getListeners(MockEvent::class)); + } + + public function testImplicitSubscriberWithMissingMethod() + { + $eventSubscriber = new TestEventSubscriberWithMissingEventMethod(); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage( + 'Cannot infer event name for missing method "Symfony\Component\EventDispatcher\Tests\TestEventSubscriberWithMissingEventMethod::invalidMethod".' + ); + + $this->dispatcher->addSubscriber($eventSubscriber); + } + + public function testImplicitSubscriberWithMissingParameter() + { + $eventSubscriber = new TestEventSubscriberWithMissingParameter(); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage( + 'Cannot infer event name for method "Symfony\Component\EventDispatcher\Tests\TestEventSubscriberWithMissingParameter::invalidMethod". Please add a type-hint for the event calls to the first parameter of the method or configure the event name explicitly.' + ); + + $this->dispatcher->addSubscriber($eventSubscriber); + } + + public function testImplicitSubscriberWithInvalidParameter() + { + $eventSubscriber = new TestEventSubscriberWithInvalidParameter(); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage( + 'Cannot infer event name for method "Symfony\Component\EventDispatcher\Tests\TestEventSubscriberWithInvalidParameter::invalidMethod". Please add a type-hint for the event calls to the first parameter of the method or configure the event name explicitly.' + ); + + $this->dispatcher->addSubscriber($eventSubscriber); + } + public function testRemoveSubscriber() { $eventSubscriber = new TestEventSubscriber(); @@ -273,6 +316,14 @@ public function testRemoveSubscriberWithMultipleListeners() $this->assertFalse($this->dispatcher->hasListeners(self::preFoo)); } + public function testRemoveSubscriberWithoutEventName() + { + $eventSubscriber = new TestEventSubscriberWithoutEventName(); + $this->dispatcher->addSubscriber($eventSubscriber); + $this->dispatcher->removeSubscriber($eventSubscriber); + $this->assertFalse($this->dispatcher->hasListeners(MockEvent::class)); + } + public function testEventReceivesTheDispatcherInstanceAsArgument() { $listener = new TestWithDispatcher(); @@ -484,3 +535,58 @@ public static function getSubscribedEvents() ]]; } } + +class TestEventSubscriberWithoutEventName implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return [ + 'onMockEvent', + ['onEarlyMockEvent', 255], + ]; + } + + public function onEarlyMockEvent(MockEvent $event): void + { + } + + public function onMockEvent(MockEvent $event): void + { + } +} + +class TestEventSubscriberWithMissingEventMethod implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return ['invalidMethod']; + } +} + +class TestEventSubscriberWithMissingParameter implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return ['invalidMethod']; + } + + public function invalidMethod(): void + { + } +} + +class TestEventSubscriberWithInvalidParameter implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return ['invalidMethod']; + } + + public function invalidMethod(string $event): void + { + } +} + +class MockEvent extends Event +{ +}