10000 Autoconfigure event listeners · symfony/symfony@4c33f8a · GitHub
[go: up one dir, main page]

Skip to content

Commit 4c33f8a

Browse files
committed
Autoconfigure event listeners
1 parent 2389d7c commit 4c33f8a

File tree

5 files changed

+146
-5
lines changed

5 files changed

+146
-5
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use Symfony\Component\DependencyInjection\Parameter;
5454
use Symfony\Component\DependencyInjection\Reference;
5555
use Symfony\Component\DependencyInjection\ServiceLocator;
56+
use Symfony\Component\EventDispatcher\EventListenerInterface;
5657
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
5758
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
5859
use Symfony\Component\Finder\Finder;
@@ -364,6 +365,8 @@ public function load(array $configs, ContainerBuilder $container)
364365
->addTag('kernel.cache_clearer');
365366
$container->registerForAutoconfiguration(CacheWarmerInterface::class)
366367
->addTag('kernel.cache_warmer');
368+
$container->registerForAutoconfiguration(EventListenerInterface::class)
369+
->addTag('kernel.event_listener');
367370
$container->registerForAutoconfiguration(EventSubscriberInterface::class)
368371
->addTag('kernel.event_subscriber');
369372
$container->registerForAutoconfiguration(ResetInterface::class)

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
* The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated
8+
* added the `EventListenerInterface` interface and guessed event name from the `__invoke` method signature.
89

910
4.1.0
1011
-----

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1515
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Definition;
1718
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1819
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\EventDispatcher\Event;
1921
use Symfony\Component\EventDispatcher\EventDispatcher;
22+
use Symfony\Component\EventDispatcher\EventListenerInterface;
2023
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
2124

2225
/**
@@ -63,22 +66,45 @@ public function process(ContainerBuilder $container)
6366
$definition = $container->findDefinition($this->dispatcherService);
6467

6568
foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) {
69+
$reflection = null;
6670
foreach ($events as $event) {
6771
$priority = isset($event['priority']) ? $event['priority'] : 0;
6872

6973
if (!isset($event['event'])) {
70-
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag));
74+
if (null === $reflection) {
75+
$class = $container->getDefinition($id)->getClass();
76+
if (!$reflection = $container->getReflectionClass($class)) {
77+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
78+
}
79+
}
80+
if (!$reflection->implementsInterface(EventListenerInterface::class)) {
81+
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags or implements the EventListenerInterface interface .', $id, $this->listenerTag));
82+
}
83+
84+
$event['event'] = $this->guessListenedClass($reflection, $id);
85+
$event['method'] = '__invoke';
86+
} else {
87+
$event['event'] = $aliases[$event['event']] ?? $event['event'];
7188
}
72-
$event['event'] = $aliases[$event['event']] ?? $event['event'];
7389

7490
if (!isset($event['method'])) {
91+
$eventName = $event['event'];
92+
if ($eventR = $container->getReflectionClass($event['event'])) {
93+
$eventName = $eventR->getShortName();
94+
}
7595
$event['method'] = 'on'.preg_replace_callback([
7696
'/(?<=\b)[a-z]/i',
7797
'/[^a-z0-9]/i',
78-
], function ($matches) { return strtoupper($matches[0]); }, $event['event']);
98+
], function ($matches) { return strtoupper($matches[0]); }, $eventName);
7999
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
80100

81-
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) {
101+
if (null === $reflection) {
102+
$class = $container->getDefinition($id)->getClass();
103+
if (!$reflection = $container->getReflectionClass($class)) {
104+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
105+
}
106+
}
107+
if (!$reflection->hasMethod($event['method']) && $reflection->hasMethod('__invoke')) {
82108
$event['method'] = '__invoke';
83109
}
84110
}
@@ -115,13 +141,47 @@ public function process(ContainerBuilder $container)
115141
$definition->addMethodCall('addListener', $args);
116142

117143
if (isset($this->hotPathEvents[$args[0]])) {
118-
$container->getDefinition($id)->addTag('container.hot_path');
144+
$container->getDefinition($id)->addTag($this->hotPathTagName);
119145
}
120146
}
121147
$extractingDispatcher->listeners = [];
122148
ExtractingEventDispatcher::$aliases = [];
123149
}
124150
}
151+
152+
private function guessListenedClass(\ReflectionClass $handlerClass, string $serviceId): string
153+
{
154+
try {
155+
$method = $handlerClass->getMethod('__invoke');
156+
} catch (\ReflectionException $e) {
157+
throw new \InvalidArgumentException(sprintf('Invalid EventListener "%s": class "%s" must have an "__invoke()" method.', $serviceId, $handlerClass->getName()));
158+
}
159+
160+
$parameters = $method->getParameters();
161+
if (1 > \count($parameters)) {
162+
throw new \InvalidArgumentException(sprintf('Invalid EventListener "%s": method "%s::__invoke()" must have, at least, one argument corresponding to the event it handles.', $serviceId, $handlerClass->getName()));
163+
}
164+
165+
if (!$type = $parameters[0]->getType()) {
166+
throw new \InvalidArgumentException(sprintf('Invalid EventListener "%s": argument "$%s" of method "%s::__invoke()" must have a type-hint corresponding to the event class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName()));
167+
}
168+
169+
if ($type->isBuiltin()) {
170+
throw new \InvalidArgumentException(sprintf('Invalid EventListener "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class, "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $type));
171+
}
172+
173+
return $parameters[0]->getType();
174+
}
175+
176+
private function registerListenerCall(ContainerBuilder $container, Definition $definition, string $listerId, string $eventName, string $method, int $priority)
177+
{
178+
$callable = [new ServiceClosureArgument(new Reference($listerId)), $method];
179+
$definition->addMethodCall('addListener', [$eventName, $callable, $priority]);
180+
181+
if (isset($this->hotPathEvents[$eventName])) {
182+
$container->getDefinition($listerId)->addTag($this->hotPathTagName);
183+
}
184+
}
125185
}
126186

127187
/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\EventDispatcher;
13+
14+
/**
15+
* Marker interface for event listeners.
16+
*
17+
* @author Jérémy Derussé <jeremy@derusse.com>
18+
*/
19+
interface EventListenerInterface
20+
{
21+
}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Reference;
1818
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
19+
use Symfony\Component\EventDispatcher\EventListenerInterface;
20+
use Symfony\Component\HttpKernel\Event\KernelEvent;
1921

2022
class RegisterListenersPassTest extends TestCase
2123
{
@@ -148,6 +150,9 @@ public function testInvokableEventListener()
148150
$container->register('foo', \stdClass::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']);
149151
$container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']);
150152
$container->register('baz', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'event']);
153+
$container->register('fqdn', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => KernelEvent::class]);
154+
$container->register('autoconfigured', AutoconfiguredListenerService::class)->addTag('kernel.event_listener');
155+
$container->register('hybride', AutoconfiguredListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo'])->addTag('kernel.event_listener');
151156
$container->register('event_dispatcher', \stdClass::class);
152157

153158
$registerListenersPass = new RegisterListenersPass();
@@ -179,6 +184,38 @@ public function testInvokableEventListener()
179184
0,
180185
],
181186
],
187+
[
188+
'addListener',
189+
[
190+
'Symfony\Component\HttpKernel\Event\KernelEvent',
191+
[new ServiceClosureArgument(new Reference('fqdn')), 'onKernelEvent'],
192+
0,
193+
],
194+
],
195+
[
196+
'addListener',
197+
[
198+
'Symfony\Component\EventDispatcher\Tests\DependencyInjection\FooEvent',
199+
[new ServiceClosureArgument(new Reference('autoconfigured')), '__invoke'],
200+
0,
201+
],
202+
],
203+
[
204+
'addListener',
205+
[
206+
'foo',
207+
[new ServiceClosureArgument(new Reference('hybride')), 'onFoo'],
208+
0,
209+
],
210+
],
211+
[
212+
'addListener',
213+
[
214+
'Symfony\Component\EventDispatcher\Tests\DependencyInjection\FooEvent',
215+
[new ServiceClosureArgument(new Reference('hybride')), '__invoke'],
216+
0,
217+
],
218+
],
182219
];
183220
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
184221
}
@@ -203,4 +240,23 @@ public function __invoke()
203240
public function onEvent()
204241
{
205242
}
243+
244+
public function onKernelEvent()
245+
{
246+
}
247+
}
248+
249+
class AutoconfiguredListenerService implements EventListenerInterface
250+
{
251+
public function __invoke(FooEvent $e)
252+
{
253+
}
254+
255+
public function onFoo(FooEvent $e)
256+
{
257+
}
258+
}
259+
260+
class FooEvent
261+
{
206262
}

0 commit comments

Comments
 (0)
0