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

Skip to content

Commit d2ebe37

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

File tree

2 files changed

+127
-18
lines changed

2 files changed

+127
-18
lines changed

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

Lines 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

Lines 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