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 d2ebe37

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

File tree

2 files changed

+127
-18
lines changed
Filter options

2 files changed

+127
-18
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php
+49-13Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,7 @@ public function process(ContainerBuilder $container): void
8080
$event['event'] = $aliases[$event['event']] ?? $event['event'];
8181

8282
if (!isset($event['method'])) {
83-
$event['method'] = 'on'.preg_replace_callback([
84-
'/(?<=\b|_)[a-z]/i',
85-
'/[^a-z0-9]/i',
86-
], fn ($matches) => strtoupper($matches[0]), $event['event']);
87-
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
88-
89-
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) {
90-
if (!$r->hasMethod('__invoke')) {
91-
throw new InvalidArgumentException(\sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "kernel.event_listener" tags.', $event['method'], $id));
92-
}
93-
94-
$event['method'] = '__invoke';
95-
}
83+
$event['method'] = $this->resolveListenerMethodFromTypeDeclaration($container, $id, $event['event'], $aliases);
9684
}
9785

9886
$dispatcherDefinition = $globalDispatcherDefinition;
@@ -183,6 +171,54 @@ private function getEventFromTypeDeclaration(ContainerBuilder $container, string
183171

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

188224
/**

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php
+78-5Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,25 @@ public function testInvokableEventListener()
202202
$container = new ContainerBuilder();
203203
$container->setParameter('event_dispatcher.event_aliases', [AliasedEvent::class => 'aliased_event']);
204204

205-
$container->register('foo', \get_class(new class {
205+
$container->register('foo', \get_class(new class() {
206206
public function onFooBar()
207207
{
208208
}
209209
}))->addTag('kernel.event_listener', ['event' => 'foo.bar']);
210210
$container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']);
211211
$container->register('baz', InvokableListenerService::class)->addTag('kernel.event_listener', ['event' => 'event']);
212-
$container->register('zar', \get_class(new class {
212+
$container->register('zar', \get_class(new class() {
213213
public function onFooBarZar()
214214
{
215215
}
216216
}))->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']);
219+
$container->register('child_event', \get_class(new class() {
220+
public function __invoke(ParentEvent $event)
221+
{
222+
}
223+
}))->addTag('kernel.event_listener', ['event' => ChildEvent::class]);
217224
$container->register('event_dispatcher', \stdClass::class);
218225

219226
$registerListenersPass = new RegisterListenersPass();
@@ -253,18 +260,68 @@ public function onFooBarZar()
253260
0,
254261
],
255262
],
263+
[
264+
'addListener',
265+
[
266+
CustomEvent::class,
267+
[new ServiceClosureArgument(new Reference('typed_listener')), 'onCustomEvent'],
268+
0,
269+
],
270+
],
271+
[
272+
'addListener',
273+
[
274+
'aliased_event',
275+
[new ServiceClosureArgument(new Reference('aliased_event')), 'onAliasedEvent'],
276+
0,
277+
],
278+
],
279+
[
280+
'addListener',
281+
[
282+
ChildEvent::class,
283+
[new ServiceClosureArgument(new Reference('child_event')), '__invoke'],
284+
0,
285+
],
286+
],
256287
];
257288
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
258289
}
259290

260-
public function testItThrowsAnExceptionIfTagIsMissingMethodAndClassHasNoValidMethod()
291+
public function testItThrowsAnExceptionIfTagIsMissingMethodAndClassHasMultipleMethodsWithEvent()
292+
{
293+
$this->expectException(InvalidArgumentException::class);
294+
$this->expectExceptionMessage('Service "foo" is missing a "method" attribute on "kernel.event_listener" tags.');
295+
296+
$container = new ContainerBuilder();
297+
298+
$container->register('foo', \get_class(new class() {
299+
public function doSomethingOnCustomEvent(CustomEvent $event)
300+
{
301+
}
302+
303+
public function doAnotherThingOnCustomEvent(CustomEvent $event)
304+
{
305+
}
306+
}))->addTag('kernel.event_listener', ['event' => CustomEvent::class]);
307+
$container->register('event_dispatcher', \stdClass::class);
308+
309+
$registerListenersPass = new RegisterListenersPass();
310+
$registerListenersPass->process($container);
311+
}
312+
313+
public function testItThrowsAnExceptionIfTagIsMissingMethodAndClassHasNoMethodWithEvent()
261314
{
262315
$this->expectException(InvalidArgumentException::class);
263-
$this->expectExceptionMessage('None of the "onFooBar" or "__invoke" methods exist for the service "foo". Please define the "method" attribute on "kernel.event_listener" tags.');
316+
$this->expectExceptionMessage('Service "foo" is missing a "method" attribute on "kernel.event_listener" tags.');
264317

265318
$container = new ContainerBuilder();
266319

267-
$container->register('foo', \stdClass::class)->addTag('kernel.event_listener', ['event' => 'foo.bar']);
320+
$container->register('foo', \get_class(new class() {
321+
public function doSomethingOnCustomEvent($event)
322+
{
323+
}
324+
}))->addTag('kernel.event_listener', ['event' => CustomEvent::class]);
268325
$container->register('event_dispatcher', \stdClass::class);
269326

270327
$registerListenersPass = new RegisterListenersPass();
@@ -524,6 +581,14 @@ public function __invoke()
524581
public function onEvent()
525582
{
526583
}
584+
585+
public function onCustomEvent(CustomEvent $event): void
586+
{
587+
}
588+
589+
public function onAliasedEvent(AliasedEvent $event): void
590+
{
591+
}
527592
}
528593

529594
final class AliasedSubscriber implements EventSubscriberInterface
@@ -541,6 +606,14 @@ final class AliasedEvent
541606
{
542607
}
543608

609+
class ParentEvent
610+
{
611+
}
612+
613+
final class ChildEvent extends ParentEvent
614+
{
615+
}
616+
544617
final class TypedListener
545618
{
546619
public function __invoke(AliasedEvent $event): void

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.