[EventDispatcher] Allow to omit the event name when registering listeners.

This commit is contained in:
Alexander M. Turek 2019-10-04 16:41:42 +02:00
parent bf406da78f
commit 6f32584c76
3 changed files with 179 additions and 1 deletions

View File

@ -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
-----

View File

@ -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;
}
}
/**

View File

@ -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
{
}
}