Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f2bcee9

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

File tree

2 files changed

+126
-11
lines changed
Filter options

2 files changed

+126
-11
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php
+48-9Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php
+78-2Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.