Skip to content

Commit 004754b

Browse files
committed
Introduce a MessageHandlerInterface interface to handle multiple messages with a handler and enable auto-configuration
1 parent 2889acf commit 004754b

File tree

4 files changed

+150
-9
lines changed

4 files changed

+150
-9
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
use Symfony\Component\Lock\LockInterface;
6060
use Symfony\Component\Lock\Store\StoreFactory;
6161
use Symfony\Component\Lock\StoreInterface;
62+
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
6263
use Symfony\Component\Messenger\Transport\ReceiverInterface;
6364
use Symfony\Component\Messenger\Transport\SenderInterface;
6465
use Symfony\Component\PropertyAccess\PropertyAccessor;
@@ -346,6 +347,8 @@ public function load(array $configs, ContainerBuilder $container)
346347
->addTag('messenger.receiver');
347348
$container->registerForAutoconfiguration(SenderInterface::class)
348349
->addTag('messenger.sender');
350+
$container->registerForAutoconfiguration(MessageHandlerInterface::class)
351+
->addTag('messenger.message_handler');
349352

350353
if (!$container->getParameter('kernel.debug')) {
351354
// remove tagged iterator argument for resource checkers

src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php

+27-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
2020
use Symfony\Component\DependencyInjection\Reference;
2121
use Symfony\Component\Messenger\Handler\ChainHandler;
22+
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
2223

2324
/**
2425
* @author Samuel Roze <samuel.roze@gmail.com>
@@ -67,16 +68,29 @@ private function registerHandlers(ContainerBuilder $container)
6768

6869
foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) {
6970
foreach ($tags as $tag) {
70-
$handles = $tag['handles'] ?? $this->guessHandledClass($r = $container->getReflectionClass($container->getParameterBag()->resolveValue($container->getDefinition($serviceId)->getClass())), $serviceId);
71-
72-
if (!class_exists($handles)) {
73-
$messageClassLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf('used as argument type in method "%s::__invoke()"', $r->getName());
74-
75-
throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $handles, $messageClassLocation));
71+
$handles = $tag['handles'] ?? $this->guessHandledClasses($r = $container->getReflectionClass($container->getParameterBag()->resolveValue($container->getDefinition($serviceId)->getClass())), $serviceId);
72+
if (empty($handles)) {
73+
throw new RuntimeException(sprintf('The handler service "%s" does not seem to handle any message.', $serviceId));
7674
}
7775

7876
$priority = $tag['priority'] ?? 0;
79-
$handlersByMessage[$handles][$priority][] = new Reference($serviceId);
77+
78+
foreach ($handles as $messageClass) {
79+
if (is_array($messageClass)) {
80+
$messagePriority = $messageClass[1];
81+
$messageClass = $messageClass[0];
82+
} else {
83+
$messagePriority = $priority;
84+
}
85+
86+
if (!class_exists($messageClass)) {
87+
$messageClassLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf($r->implementsInterface(MessageHandlerInterface::class) ? 'returned by method "%s::getHandledMessages()"' : 'used as argument type in method "%s::__invoke()"', $r->getName());
88+
89+
throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $messageClass, $messageClassLocation));
90+
}
91+
92+
$handlersByMessage[$messageClass][$messagePriority][] = new Reference($serviceId);
93+
}
8094
}
8195
}
8296

@@ -108,8 +122,12 @@ private function registerHandlers(ContainerBuilder $container)
108122
$handlerResolver->replaceArgument(0, ServiceLocatorTagPass::register($container, $handlersLocatorMapping));
109123
}
110124

111-
private function guessHandledClass(\ReflectionClass $handlerClass, string $serviceId): string
125+
private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId): array
112126
{
127+
if ($handlerClass->implementsInterface(MessageHandlerInterface::class)) {
128+
return $handlerClass->getName()::getHandledMessages();
129+
}
130+
113131
try {
114132
$method = $handlerClass->getMethod('__invoke');
115133
} catch (\ReflectionException $e) {
@@ -129,7 +147,7 @@ private function guessHandledClass(\ReflectionClass $handlerClass, string $servi
129147
throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $type));
130148
}
131149

132-
return $parameters[0]->getType();
150+
return array((string) $parameters[0]->getType());
133151
}
134152

135153
private function registerReceivers(ContainerBuilder $container)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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\Component\Messenger\Handler;
13+
14+
/**
15+
* Handlers can implement this interface. This allows them to be auto-configured and to
16+
* handle multiple messages.
17+
*
18+
* @author Samuel Roze <samuel.roze@gmail.com>
19+
*/
20+
interface MessageHandlerInterface
21+
{
22+
/**
23+
* Return a list of messages to be handled.
24+
*
25+
* It returns a list of messages like in the following example:
26+
*
27+
* return [MyMessage::class];
28+
*
29+
* It can also change the priority per classes.
30+
*
31+
* return [
32+
* [FirstMessage::class, 0],
33+
* [SecondMessage::class, -10],
34+
* ];
35+
*
36+
* The `__invoke` method of the handler will be called as usual with the message to handle.
37+
*
38+
* @return array
39+
*/
40+
public static function getHandledMessages(): array;
41+
}

src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php

+79
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
use Symfony\Component\DependencyInjection\ServiceLocator;
1919
use Symfony\Component\Messenger\ContainerHandlerLocator;
2020
use Symfony\Component\Messenger\DependencyInjection\MessengerPass;
21+
use Symfony\Component\Messenger\Handler\ChainHandler;
22+
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
2123
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
24+
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
2225
use Symfony\Component\Messenger\Transport\ReceiverInterface;
2326

2427
class MessengerPassTest extends TestCase
@@ -50,6 +53,34 @@ public function testProcess()
5053
);
5154
}
5255

56+
public function testGetClassesFromTheHandlerInterface()
57+
{
58+
$container = $this->getContainerBuilder();
59+
$container
60+
->register(HandlerWithMultipleMessages::class, HandlerWithMultipleMessages::class)
61+
->addTag('messenger.message_handler')
62+
;
63+
$container
64+
->register(PrioritisedHandler::class, PrioritisedHandler::class)
65+
->addTag('messenger.message_handler')
66+
;
67+
68+
(new MessengerPass())->process($container);
69+
70+
$handlerLocatorDefinition = $container->getDefinition($container->getDefinition('messenger.handler_resolver')->getArgument(0));
71+
$handlerMapping = $handlerLocatorDefinition->getArgument(0);
72+
73+
$this->assertArrayHasKey('handler.'.DummyMessage::class, $handlerMapping);
74+
$this->assertEquals(new ServiceClosureArgument(new Reference(HandlerWithMultipleMessages::class)), $handlerMapping['handler.'.DummyMessage::class]);
75+
76+
$this->assertArrayHasKey('handler.'.SecondMessage::class, $handlerMapping);
77+
$handlerReference = (string) $handlerMapping['handler.'.SecondMessage::class]->getValues()[0];
78+
$definition = $container->getDefinition($handlerReference);
79+
80+
$this->assertEquals(ChainHandler::class, $definition->getClass());
81+
$this->assertEquals(array(new Reference(PrioritisedHandler::class), new Reference(HandlerWithMultipleMessages::class)), $definition->getArgument(0));
82+
}
83+
5384
/**
5485
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
5586
* @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler": message class "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler::__invoke()" does not exist.
@@ -65,6 +96,21 @@ public function testUndefinedMessageClassForHandler()
6596
(new MessengerPass())->process($container);
6697
}
6798

99+
/**
100+
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
101+
* @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaInterface": message class "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaInterface::getHandledMessages()" does not exist.
102+
*/
103+
public function testUndefinedMessageClassForHandlerViaInterface()
104+
{
105+
$container = $this->getContainerBuilder();
106+
$container
107+
->register(UndefinedMessageHandlerViaInterface::class, UndefinedMessageHandlerViaInterface::class)
108+
->addTag('messenger.message_handler')
109+
;
110+
111+
(new MessengerPass())->process($container);
112+
}
113+
68114
/**
69115
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
70116
* @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\NotInvokableHandler": class "Symfony\Component\Messenger\Tests\DependencyInjection\NotInvokableHandler" must have an "__invoke()" method.
@@ -168,6 +214,18 @@ public function __invoke(UndefinedMessage $message)
168214
}
169215
}
170216

217+
class UndefinedMessageHandlerViaInterface implements MessageHandlerInterface
218+
{
219+
public static function getHandledMessages(): array
220+
{
221+
return array(UndefinedMessage::class);
222+
}
223+
224+
public function __invoke()
225+
{
226+
}
227+
}
228+
171229
class NotInvokableHandler
172230
{
173231
}
@@ -192,3 +250,24 @@ public function __invoke(string $message)
192250
{
193251
}
194252
}
253+
254+
class HandlerWithMultipleMessages implements MessageHandlerInterface
255+
{
256+
public static function getHandledMessages(): array
257+
{
258+
return array(
259+
DummyMessage::class,
260+
SecondMessage::class,
261+
);
262+
}
263+
}
264+
265+
class PrioritisedHandler implements MessageHandlerInterface
266+
{
267+
public static function getHandledMessages(): array
268+
{
269+
return array(
270+
array(SecondMessage::class, 10),
271+
);
272+
}
273+
}

0 commit comments

Comments
 (0)