8000 [EventDispatcher] Improve method resolving when it is omitted in tag · symfony/symfony@f2bcee9 · GitHub
[go: up one dir, main page]

Skip to content

Commit f2bcee9

Browse files
committed
[EventDispatcher] Improve method resolving when it is omitted in tag
1 parent a8872de commit f2bcee9

File tree

2 files changed

+126
-11
lines changed

2 files changed

+126
-11
lines changed

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

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,7 @@ public function process(ContainerBuilder $container)
8383
$event['event'] = $aliases[$event['event']] ?? $event['event'];
8484

8585
if (!isset($event['method'])) {
86-
$event['method'] = 'on'.preg_replace_callback([
87-
'/(?<=\b|_)[a-z]/i',
88-
'/[^a-z0-9]/i',
89-
], fn ($matches) => strtoupper($matches[0]), $event['event']);
90-
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
91-
92-
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) {
93-
$event['method'] = '__invoke';
94-
}
86+
$event['method'] = $this->resolveListenerMethodFromTypeDeclaration($container, $id, $event['event'], $aliases);
9587
}
9688

9789
$dispatcherDefinition = $globalDispatcherDefinition;
@@ -182,6 +174,53 @@ private function getEventFromTypeDeclaration(ContainerBuilder $container, string
182174

183175
return $name;
184176
}
177+
178+
/**
179+
* Resolve the listener method:
180+
* - look for a method named after the event (camelized, with "on" prefix)
181+
* - if such a method does not exist, check if only one public method exist which expect the event as parameter
182+
* - otherwise, throw an exception because we need more information.
183+
*/
184+
private function resolveListenerMethodFromTypeDeclaration(ContainerBuilder $container, string $id, string $eventName, array $aliases): string
185+
{
186+
$method = 'on'.preg_replace_callback([
187+
'/(?<=\b|_)[a-z]/i',
188+
'/[^a-z0-9]/i',
189+
], fn ($matches) => strtoupper($matches[0]), $eventName);
190+
$method = preg_replace('/[^a-z0-9]/i', '', $method);
191+
192+
if (
193+
null === ($class = $container->getDefinition($id)->getClass())
194+
|| !($r = $container->getReflectionClass($class, false))
195+
|| $r->hasMethod($method)
196+
) {
197+
return $method;
198+
}
199+
200+
$publicMethods = $r->getMethods(\ReflectionMethod::IS_PUBLIC);
201+
$eventName = array_flip($aliases)[$eventName] ?? $eventName;
202+
203+
$candidateMethods = [];
204+
foreach ($publicMethods as $publicMethod) {
205+
if (
206+
1 === $publicMethod->getNumberOfParameters()
207+
&& ($type = $publicMethod->getParameters()[0]->getType()) instanceof \ReflectionNamedType
208+
&& $type->getName() === $eventName
209+
) {
210+
$candidateMethods[] = $publicMethod->getName();
211+
}
212+
}
213+
214+
if (1 === \count($candidateMethods)) {
215+
return $candidateMethods[0];
216+
}
217+
218+
if ($r->hasMethod('__invoke')) {
219+
return '__invoke';
220+
}
221+
222+
throw new InvalidArgumentException(sprintf('Service "%s" must define the "method" attribute on "kernel.event_listener" tags.', $id));
223+
}
185224
}
186225

187226
/**

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

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,22 @@ public function testEventSubscriberUnresolvableClassName()
200200
public function testInvokableEventListener()
201201
{
202202
$container = new ContainerBuilder();
203-
$container->register('foo', \stdClass::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']);
203+
$container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']);
204+
205+
$container->register('foo', \get_class(new class() {
206+
public function onFooBar()
207+
{
208+
}
209+
}))->addTag('kernel.event_listener', ['event' => 'foo.bar']);
204210
$container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']);
205211
$container->register('baz', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'event']);
206-
$container->register('zar', \stdClass::class)->addTag('kernel.event_listener', ['event' => 'foo.bar_zar']);
212+
$container->register('zar', \get_class(new class() {
213+
public function onFooBarZar()
214+
{
215+
}
216+
}))->addTag('kernel.event_listener', ['event' => 'foo.bar_zar']);
217+
$container->register('typed_listener', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => CustomEvent::class]);
218+
$container->register('aliased_event', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'aliased_event']);
207219
$container->register('event_dispatcher', \stdClass::class);
208220

209221
$registerListenersPass = new RegisterListenersPass();
@@ -243,10 +255,66 @@ public function testInvokableEventListener()
243255
0,
244256
],
245257
],
258+
[
259+
'addListener',
260+
[
261+
CustomEvent::class,
262+
[new ServiceClosureArgument(new Reference('typed_listener')), 'onCustomEvent'],
263+
0,
264+
],
265+
],
266+
[
267+
'addListener',
268+
[
269+
'aliased_event',
270+
[new ServiceClosureArgument(new Reference('aliased_event')), 'onAliasedEvent'],
271+
0,
272+
],
273+
],
246274
];
247275
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
248276
}
249277

278+
public function testItThrowsAnExceptionIfTagIsMissingMethodAndClassHasMultipleMethodsWithEvent()
279+
{
280+
$this->expectException(InvalidArgumentException::class);
281+
$this->expectExceptionMessage('Service "foo" must define the "method" attribute on "kernel.event_listener" tags.');
282+
283+
$container = new ContainerBuilder();
284+
285+
$container->register('foo', \get_class(new class() {
286+
public function doSomethingOnCustomEvent(CustomEvent $event)
287+
{
288+
}
289+
290+
public function doAnotherThingOnCustomEvent(CustomEvent $event)
291+
{
292+
}
293+
}))->addTag('kernel.event_listener', ['event' => CustomEvent::class]);
294+
$container->register('event_dispatcher', \stdClass::class);
295+
296+
$registerListenersPass = new RegisterListenersPass();
297+
$registerListenersPass->process($container);
298+
}
299+
300+
public function testItThrowsAnExceptionIfTagIsMissingMethodAndClassHasNoMethodWithEvent()
301+
{
302+
$this->expectException(InvalidArgumentException::class);
303+
$this->expectExceptionMessage('Service "foo" must define the "method" attribute on "kernel.event_listener" tags.');
304+
305+
$container = new ContainerBuilder();
306+
307+
$container->register('foo', \get_class(new class() {
308+
public function doSomethingOnCustomEvent($event)
309+
{
310+
}
311+
}))->addTag('kernel.event_listener', ['event' => CustomEvent::class]);
312+
$container->register('event_dispatcher', \stdClass::class);
313+
314+
$registerListenersPass = new RegisterListenersPass();
315+
$registerListenersPass->process($container);
316+
}
317+
250318
public function testTaggedInvokableEventListener()
251319
{
252320
$container = new ContainerBuilder();
@@ -500,6 +568,14 @@ public function __invoke()
500568
public function onEvent()
501569
{
502570
}
571+
572+
public function onCustomEvent(CustomEvent $event): void
573+
{
574+
}
575+
576+
public function onAliasedEvent(AliasedEvent $event): void
577+
{
578+
}
503579
}
504580

505581
final class AliasedSubscriber implements EventSubscriberInterface

0 commit comments

Comments
 (0)
0