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

Skip to content

Commit 4c13714

Browse files
committed
Autoconfigure event listeners
1 parent 8da7686 commit 4c13714

File tree

4 files changed

+141
-4
lines changed

4 files changed

+141
-4
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
117117
use Symfony\Component\Yaml\Yaml;
118118
use Symfony\Contracts\Cache\CacheInterface;
119+
use Symfony\Contracts\EventDispatcher\EventListenerInterface;
119120
use Symfony\Contracts\HttpClient\HttpClientInterface;
120121
use Symfony\Contracts\Service\ResetInterface;
121122
use Symfony\Contracts\Service\ServiceSubscriberInterface;
@@ -368,6 +369,8 @@ public function load(array $configs, ContainerBuilder $container)
368369
->addTag('kernel.cache_clearer');
369370
$container->registerForAutoconfiguration(CacheWarmerInterface::class)
370371
->addTag('kernel.cache_warmer');
372+
$container->registerForAutoconfiguration(EventListenerInterface::class)
373+
->addTag('kernel.event_listener');
371374
$container->registerForAutoconfiguration(EventSubscriberInterface::class)
372375
->addTag('kernel.event_subscriber');
373376
$container->registerForAutoconfiguration(ResetInterface::class)

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +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;
1920
use Symfony\Component\EventDispatcher\EventDispatcher;
2021
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
22+
use Symfony\Contracts\EventDispatcher\EventListenerInterface;
2123

2224
/**
2325
* Compiler pass to register tagged services for an event dispatcher.
@@ -63,22 +65,41 @@ public function process(ContainerBuilder $container)
6365
$definition = $container->findDefinition($this->dispatcherService);
6466

6567
foreach ($container->findTaggedServiceIds($this->listenerTag, true) as $id => $events) {
68+
$reflection = null;
6669
foreach ($events as $event) {
6770
$priority = isset($event['priority']) ? $event['priority'] : 0;
6871

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

7489
if (!isset($event['method'])) {
7590
$event['method'] = 'on'.preg_replace_callback([
7691
'/(?<=\b)[a-z]/i',
7792
'/[^a-z0-9]/i',
78-
], function ($matches) { return strtoupper($matches[0]); }, $event['event']);
93+
], function ($matches) { return strtoupper($matches[0]); }, (false === $p = strrpos($event['event'], '\\')) ? $event['event'] : substr($event['event'], $p + 1));
7994
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
8095

81-
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) {
96+
if (null === $reflection) {
97+
$class = $container->getDefinition($id)->getClass();
98+
if (!$reflection = $container->getReflectionClass($class)) {
99+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
100+
}
101+
}
102+
if (!$reflection->hasMethod($event['method']) && $reflection->hasMethod('__invoke')) {
82103
$event['method'] = '__invoke';
83104
}
84105
}
@@ -122,6 +143,40 @@ public function process(ContainerBuilder $container)
122143
ExtractingEventDispatcher::$aliases = [];
123144
}
124145
}
146+
147+
private function guessListenedClass(\ReflectionClass $handlerClass, string $serviceId): string
148+
{
149+
try {
150+
$method = $handlerClass->getMethod('__invoke');
151+
} catch (\ReflectionException $e) {
152+
throw new \InvalidArgumentException(sprintf('Invalid EventListener "%s": class "%s" must have an "__invoke()" method.', $serviceId, $handlerClass->getName()));
153+
}
154+
155+
$parameters = $method->getParameters();
156+
if (1 > \count($parameters)) {
157+
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()));
158+
}
159+
160+
if (!$type = $parameters[0]->getType()) {
161+
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()));
162+
}
163+
164+
if ($type->isBuiltin()) {
165+
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));
166+
}
167+
168+
return $parameters[0]->getType();
169+
}
170+
171+
private function registerListenerCall(ContainerBuilder $container, Definition $definition, string $listerId, string $eventName, string $method, int $priority)
172+
{
173+
$callable = [new ServiceClosureArgument(new Reference($listerId)), $method];
174+
$definition->addMethodCall('addListener', [$eventName, $callable, $priority]);
175+
176+
if (isset($this->hotPathEvents[$eventName])) {
177+
$container->getDefinition($listerId)->addTag($this->hotPathTagName);
178+
}
179+
}
125180
}
126181

127182
/**

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\HttpKernel\Event\KernelEvent;
20+
use Symfony\Contracts\EventDispatcher\EventListenerInterface;
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
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Contracts\EventDispatcher;
13+
14+
/**
15+
* Marker interface for event listeners.
16+
*
17+
* @method void __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher)
18+
*
19+
* @author Jérémy Derussé <jeremy@derusse.com>
20+
*/
21+
interface EventListenerInterface
22+
{
23+
}

0 commit comments

Comments
 (0)
0