8000 [EventDispatcher] Allow to omit the event name when registering liste… · symfony/symfony@da25786 · GitHub
[go: up one dir, main page]

Skip to content

Commit da25786

Browse files
committed
[EventDispatcher] Allow to omit the event name when registering listeners.
1 parent 8f92594 commit da25786

File tree

3 files changed

+179
-1
lines changed

3 files changed

+179
-1
lines changed

src/Symfony/Component/EventDispatcher/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`.
8+
* Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events.
89

910
4.3.0
1011
-----

src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1818
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Component\EventDispatcher\Event as LegacyEvent;
1920
use Symfony\Component\EventDispatcher\EventDispatcher;
2021
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
22+
use Symfony\Contracts\EventDispatcher\Event;
2123

2224
/**
2325
* Compiler pass to register tagged services for an event dispatcher.
@@ -67,8 +69,14 @@ public function process(ContainerBuilder $container)
6769
$priority = isset($event['priority']) ? $event['priority'] : 0;
6870

6971
if (!isset($event['event'])) {
70-
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag));
72+
if ($container->getDefinition($id)->hasTag($this->subscriberTag)) {
73+
continue;
74+
}
75+
76+
$event['method'] = $event['method'] ?? '__invoke';
77+
$event['event'] = $this->getEventTypeDeclaration($container, $id, $event['method']);
7178
}
79+
7280
$event['event'] = $aliases[$event['event']] ?? $event['event'];
7381

7482
if (!isset($event['method'])) {
@@ -122,6 +130,24 @@ public function process(ContainerBuilder $container)
122130
ExtractingEventDispatcher::$aliases = [];
123131
}
124132
}
133+
134+
private function getEventTypeDeclaration(ContainerBuilder $container, string $id, string $method): string
135+
{
136+
if (
137+
null === ($class = $container->getDefinition($id)->getClass())
138+
|| !($r = $container->getReflectionClass($class, false))
139+
|| !$r->hasMethod($method)
140+
|| 1 > ($m = $r->getMethod($method))->getNumberOfParameters()
141+
|| !($type = $m->getParameters()[0]->getType())
142+
|| $type->isBuiltin()
143+
|| Event::class === ($name = $type->getName())
144+
|| LegacyEvent::class === $name
145+
) {
146+
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag));
147+
}
148+
149+
return $name;
150+
}
125151
}
126152

127153
/**

src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1718
use Symfony\Component\DependencyInjection\Reference;
1819
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
1920
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
@@ -244,6 +245,116 @@ public function testAliasedEventListener(): void
244245
];
245246
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
246247
}
248+
249+
public function testOmitEventNameOnTypedListener(): void
250+
{
251+
$container = new ContainerBuilder();
252+
$container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']);
253+
$container->register('foo', TypedListener::class)->addTag('kernel.event_listener', ['method' => 'onEvent']);
254+
$container->register('bar', TypedListener::class)->addTag('kernel.event_listener');
255+
$container->register('event_dispatcher');
256+
257+
$registerListenersPass = new RegisterListenersPass();
258+
$registerListenersPass->process($container);
259+
260+
$definition = $container->getDefinition('event_dispatcher');
261+
$expectedCalls = [
262+
[
263+
'addListener',
264+
[
265+
CustomEvent::class,
266+
[new ServiceClosureArgument(new Reference('foo')), 'onEvent'],
267+
0,
268+
],
269+
],
270+
[
271+
'addListener',
272+
[
273+
'aliased_event',
274+
[new ServiceClosureArgument(new Reference('bar')), '__invoke'],
275+
0,
276+
],
277+
],
278+
];
279+
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
280+
}
281+
282+
public function testOmitEventNameOnUntypedListener(): void
283+
{
284+
$container = new ContainerBuilder();
285+
$container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener', ['method' => 'onEvent']);
286+
$container->register('event_dispatcher');
287+
288+
$this->expectException(InvalidArgumentException::class);
289+
$this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.');
290+
291+
$registerListenersPass = new RegisterListenersPass();
292+
$registerListenersPass->process($container);
293+
}
294+
295+
public function testOmitEventNameAndMethodOnUntypedListener(): void
296+
{
297+
$container = new ContainerBuilder();
298+
$container->register('foo', InvokableListenerService::class)->addTag('kernel.event_listener');
299+
$container->register('event_dispatcher');
300+
301+
$this->expectException(InvalidArgumentException::class);
302+
$this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.');
303+
304+
$registerListenersPass = new RegisterListenersPass();
305+
$registerListenersPass->process($container);
306+
}
307+
308+
/**
309+
* @requires PHP 7.2
310+
*/
311+
public function testOmitEventNameAndMethodOnGenericListener(): void
312+
{
313+
$container = new ContainerBuilder();
314+
$container->register('foo', GenericListener::class)->addTag('kernel.event_listener');
315+
$container->register('event_dispatcher');
316+
317+
$this->expectException(InvalidArgumentException::class);
318+
$this->expectExceptionMessage('Service "foo" must define the "event" attribute on "kernel.event_listener" tags.');
319+
320+
$registerListenersPass = new RegisterListenersPass();
321+
$registerListenersPass->process($container);
322+
}
323+
324+
public function testOmitEventNameOnSubscriber(): void
325+
{
326+
$container = new ContainerBuilder();
327+
$container->register('subscriber', IncompleteSubscriber::class)
328+
->addTag('kernel.event_subscriber')
329+
->addTag('kernel.event_listener')
330+
->addTag('kernel.event_listener', ['event' => 'bar', 'method' => 'onBar'])
331+
;
332+
$container->register('event_dispatcher');
333+
334+
$registerListenersPass = new RegisterListenersPass();
335+
$registerListenersPass->process($container);
336+
337+
$definition = $container->getDefinition('event_dispatcher');
338+
$expectedCalls = [
339+
[
340+
'addListener',
341+
[
342+
'bar',
343+
[new ServiceClosureArgument(new Reference('subscriber')), 'onBar'],
344+
0,
345+
],
346+
],
347+
[
348+
'addListener',
349+
[
350+
'foo',
351+
[new ServiceClosureArgument(new Reference('subscriber')), 'onFoo'],
352+
0,
353+
],
354+
],
355+
];
356+
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
357+
}
247358
}
248359

249360
class SubscriberService implements EventSubscriberInterface
@@ -285,3 +396,43 @@ final class AliasedEvent
285396
final class CustomEvent
286397
{
287398
}
399+
400+
final class TypedListener
401+
{
402+
public function __invoke(AliasedEvent $event): void
403+
{
404+
}
405+
406+
public function onEvent(CustomEvent $event): void
407+
{
408+
}
409+
}
410+
411+
final class GenericListener
412+
{
413+
public function __invoke(object $event): void
414+
{
415+
}
416+
}
417+
418+
final class IncompleteSubscriber implements EventSubscriberInterface
419+
{
420+
public static function getSubscribedEvents(): array
421+
{
422+
return [
423+
'foo' => 'onFoo',
424+
];
425+
}
426+
427+
public function onFoo(): void
428+
{
429+
}
430+
431+
public function onBar(): void
432+
{
433+
}
434+
435+
public function __invoke(CustomEvent $event): void
436+
{
437+
}
438+
}

0 commit comments

Comments
 (0)
0