8000 Autoconfigure event listeners · symfony/symfony@076e7e6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 076e7e6

Browse files
committed
Autoconfigure event listeners
1 parent 162d5a8 commit 076e7e6

File tree

4 files changed

+149
-29
lines changed

4 files changed

+149
-29
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/DependencyInjection/RegisterListenersPass.php

Lines changed: 68 additions & 29 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
/**
@@ -64,44 +67,51 @@ public function process(ContainerBuilder $container)
6467

6568
foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) {
6669
foreach ($events as $event) {
67-
$priority = isset($event['priority']) ? $event['priority'] : 0;
68-
69-
if (!isset($event['event'])) {
70-
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag));
70+
$class = $container->getDefinition($id)->getClass();
71+
if (!$r = $container->getReflectionClass($class)) {
72+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
7173
}
72-
$event['event'] = $aliases[$event['event']] ?? $event['event'];
7374

74-
if (!isset($event['method'])) {
75-
$event['method'] = 'on'.preg_replace_callback([
76-
'/(?<=\b)[a-z]/i',
77-
'/[^a-z0-9]/i',
78-
], function ($matches) { return strtoupper($matches[0]); }, $event['event']);
79-
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
80-
81-
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) {
82-
$event['method'] = '__invoke';
75+
$priority = isset($event['priority']) ? $event['priority'] : 0;
76+
if (isset($event['event'])) {
77+
$event['event'] = $aliases[$event['event']] ?? $event['event'];
78+
$eventName = $event['event'];
79+
if ($eventR = $container->getReflectionClass($event['event'])) {
80+
$eventName = $eventR->getShortName();
8381
}
84-
}
8582

86-
$definition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]);
83+
if (!isset($event['method'])) {
84+
$event['method'] = 'on'.preg_replace_callback([
85+
'/(?<=\b)[a-z]/i',
86+
'/[^a-z0-9]/i',
87+
], function ($matches) { return strtoupper($matches[0]); }, $eventName);
88+
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
89+
90+
if (!$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) {
91+
$event['method'] = '__invoke';
92+
}
93+
}
94+
} else {
95+
if (!$r->implementsInterface(EventListenerInterface::class)) {
96+
throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags or implements the EventListenerInterface interface .', $id, $this->listenerTag));
97+
}
8798

88-
if (isset($this->hotPathEvents[$event['event']])) {
89-
$container->getDefinition($id)->addTag($this->hotPathTagName);
99+
$event['event'] = $this->guessListenedClass($r, $id);
100+
$event['method'] = '__invoke';
90101
}
102+
103+
$this->registerListenerCall($container, $definition, $id, $event['event'], $event['method'], $priority);
91104
}
92105
}
93106

94107
$extractingDispatcher = new ExtractingEventDispatcher();
95108

96109
foreach ($container->findTaggedServiceIds($this->subscriberTag, true) as $id => $attributes) {
97-
$def = $container->getDefinition($id);
98-
99-
// We must assume that the class value has been correctly filled, even if the service is created by a factory
100-
$class = $def->getClass();
101-
110+
$class = $container->getDefinition($id)->getClass();
102111
if (!$r = $container->getReflectionClass($class)) {
103112
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
104113
}
114+
105115
if (!$r->isSubclassOf(EventSubscriberInterface::class)) {
106116
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class));
107117
}
@@ -111,17 +121,46 @@ public function process(ContainerBuilder $container)
111121
ExtractingEventDispatcher::$subscriber = $class;
112122
$extractingDispatcher->addSubscriber($extractingDispatcher);
113123
foreach ($extractingDispatcher->listeners as $args) {
114-
$args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]];
115-
$definition->addMethodCall('addListener', $args);
116-
117-
if (isset($this->hotPathEvents[$args[0]])) {
118-
$container->getDefinition($id)->addTag('container.hot_path');
119-
}
124+
$this->registerListenerCall($container, $definition, $id, ...$args);
120125
}
121126
$extractingDispatcher->listeners = [];
122127
ExtractingEventDispatcher::$aliases = [];
123128
}
124129
}
130+
131+
private function guessListenedClass(\ReflectionClass $handlerClass, string $serviceId): string
132+
{
133+
try {
134+
$method = $handlerClass->getMethod('__invoke');
135+
} catch (\ReflectionException $e) {
136+
throw new \InvalidArgumentException(sprintf('Invalid EventListener "%s": class "%s" must have an "__invoke()" method.', $serviceId, $handlerClass->getName()));
137+
}
138+
139+
$parameters = $method->getParameters();
140+
if (1 > \count($parameters)) {
141+
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()));
142+
}
143+
144+
if (!$type = $parameters[0]->getType()) {
145+
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()));
146+
}
147+
148+
if ($type->isBuiltin()) {
149+
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));
150+
}
151+
152+
return $parameters[0]->getType();
153+
}
154+
155+
private function registerListenerCall(ContainerBuilder $container, Definition $definition, string $listerId, string $eventName, string $method, int $priority)
156+
{
157+
$callable = [new ServiceClosureArgument(new Reference($listerId)), $method];
158+
$definition->addMethodCall('addListener', [$eventName, $callable, $priority]);
159+
160+
if (isset($this->hotPathEvents[$eventName])) {
161+
$container->getDefinition($listerId)->addTag($this->hotPathTagName);
162+
}
163+
}
125164
}
126165

127166
/**
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: 57 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,24 @@ public function __invoke()
203240
public function onEvent()
204241
{
205242
}
243+
244+
public function onKernelEvent()
245+
{
246+
247+
}
248+
}
249+
250+
class AutoconfiguredListenerService implements EventListenerInterface
251+
{
252+
public function __invoke(FooEvent $e)
253+
{
254+
}
255+
256+
public function onFoo(FooEvent $e)
257+
{
258+
}
259+
}
260+
261+
class FooEvent
262+
{
206263
}

0 commit comments

Comments
 (0)
0