8000 feature #33851 [EventDispatcher] Allow to omit the event name when re… · symfony/symfony@0094884 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0094884

Browse files
feature #33851 [EventDispatcher] Allow to omit the event name when registering listeners (derrabus)
This PR was merged into the 4.4 branch. Discussion ---------- [EventDispatcher] Allow to omit the event name when registering listeners | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #33453 (kind of) | License | MIT | Doc PR | TODO After #30801 and #33485, this is another attempt at taking advantage of FQCN events for simplifying the registration of event listeners by inferring the event name from the parameter type declaration of the listener. This is my last attempt, I promise. 🙈 This time, I'd like to make the `event` attribute of the `kernel.event_listener` tag optional. This would allow us to build listeners like the following one without adding any attributes to the `kernel.event_listener` tag. ```php namespace App\EventListener; final class MyRequestListener { public function __invoke(RequestEvent $event): void { // do something } } ``` This in turn allows us to register a whole namespace of such listeners without having to configure each listener individually: ```YAML services: App\EventListener\: resource: ../src/EventListener/* tags: [kernel.event_listener] ``` Commits ------- 6f32584 [EventDispatcher] Allow to omit the event name when registering listeners.
2 parents fbf55c2 + 6f32584 commit 0094884

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->getEventFromTypeDeclaration($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 getEventFromTypeDeclaration(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