Skip to content

Commit

Permalink
Infer events a subscriber subscribes to from listener parameters.
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Mar 31, 2019
1 parent 9bcea2e commit 359870e
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 1 deletion.
Expand Up @@ -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);
}
}
41 changes: 41 additions & 0 deletions src/Symfony/Component/EventDispatcher/EventDispatcher.php
Expand Up @@ -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])) {
Expand All @@ -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]]);
Expand Down Expand Up @@ -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.
*/
Expand Down
Expand Up @@ -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();
Expand Down
Expand Up @@ -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
{
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
{
}

0 comments on commit 359870e

Please sign in to comment.