feature #26685 [Messenger] Add a MessageHandlerInterface
(multiple messages + auto-configuration) (sroze)
This PR was squashed before being merged into the 4.1-dev branch (closes #26685).
Discussion
----------
[Messenger] Add a `MessageHandlerInterface` (multiple messages + auto-configuration)
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | ø
| License | MIT
| Doc PR | ø
Based on @chalasr's PR: https://github.com/symfony/symfony/pull/26672.
This reduces the hassle of registering handlers: it allows the auto-configuration with a new interface `HandlerInterface`. At the same time, it allows a handler to handle multiple messages.
Commits
-------
07e6bc73a3
[Messenger] Add a `MessageHandlerInterface` (multiple messages + auto-configuration)
This commit is contained in:
commit
d5b88eb7eb
@ -60,6 +60,7 @@ use Symfony\Component\Lock\Lock;
|
||||
use Symfony\Component\Lock\LockInterface;
|
||||
use Symfony\Component\Lock\Store\StoreFactory;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Symfony\Component\Messenger\Transport\ReceiverInterface;
|
||||
use Symfony\Component\Messenger\Transport\SenderInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessor;
|
||||
@ -347,6 +348,8 @@ class FrameworkExtension extends Extension
|
||||
->addTag('messenger.receiver');
|
||||
$container->registerForAutoconfiguration(SenderInterface::class)
|
||||
->addTag('messenger.sender');
|
||||
$container->registerForAutoconfiguration(MessageHandlerInterface::class)
|
||||
->addTag('messenger.message_handler');
|
||||
|
||||
if (!$container->getParameter('kernel.debug')) {
|
||||
// remove tagged iterator argument for resource checkers
|
||||
|
@ -19,6 +19,8 @@ use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\Messenger\Handler\ChainHandler;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
|
||||
|
||||
/**
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
@ -67,16 +69,25 @@ class MessengerPass implements CompilerPassInterface
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) {
|
||||
foreach ($tags as $tag) {
|
||||
$handles = $tag['handles'] ?? $this->guessHandledClass($r = $container->getReflectionClass($container->getParameterBag()->resolveValue($container->getDefinition($serviceId)->getClass())), $serviceId);
|
||||
|
||||
if (!class_exists($handles)) {
|
||||
$messageClassLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf('used as argument type in method "%s::__invoke()"', $r->getName());
|
||||
|
||||
throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $handles, $messageClassLocation));
|
||||
}
|
||||
|
||||
$handles = $tag['handles'] ?? $this->guessHandledClasses($r = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()), $serviceId);
|
||||
$priority = $tag['priority'] ?? 0;
|
||||
$handlersByMessage[$handles][$priority][] = new Reference($serviceId);
|
||||
|
||||
foreach ($handles as $messageClass) {
|
||||
if (is_array($messageClass)) {
|
||||
$messagePriority = $messageClass[1];
|
||||
$messageClass = $messageClass[0];
|
||||
} else {
|
||||
$messagePriority = $priority;
|
||||
}
|
||||
|
||||
if (!class_exists($messageClass)) {
|
||||
$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());
|
||||
|
||||
throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $messageClass, $messageClassLocation));
|
||||
}
|
||||
|
||||
$handlersByMessage[$messageClass][$messagePriority][] = new Reference($serviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,8 +119,16 @@ class MessengerPass implements CompilerPassInterface
|
||||
$handlerResolver->replaceArgument(0, ServiceLocatorTagPass::register($container, $handlersLocatorMapping));
|
||||
}
|
||||
|
||||
private function guessHandledClass(\ReflectionClass $handlerClass, string $serviceId): string
|
||||
private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId): array
|
||||
{
|
||||
if ($handlerClass->implementsInterface(MessageSubscriberInterface::class)) {
|
||||
if (!$handledMessages = $handlerClass->getName()::getHandledMessages()) {
|
||||
throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::getHandledMessages()" must return one or more messages.', $serviceId, $handlerClass->getName()));
|
||||
}
|
||||
|
||||
return $handledMessages;
|
||||
}
|
||||
|
||||
try {
|
||||
$method = $handlerClass->getMethod('__invoke');
|
||||
} catch (\ReflectionException $e) {
|
||||
@ -129,7 +148,7 @@ class MessengerPass implements CompilerPassInterface
|
||||
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));
|
||||
}
|
||||
|
||||
return $parameters[0]->getType();
|
||||
return array((string) $parameters[0]->getType());
|
||||
}
|
||||
|
||||
private function registerReceivers(ContainerBuilder $container)
|
||||
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Handler;
|
||||
|
||||
/**
|
||||
* Handlers can implement this interface.
|
||||
*
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
interface MessageHandlerInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Messenger\Handler;
|
||||
|
||||
/**
|
||||
* Handlers can implement this interface to handle multiple messages.
|
||||
*
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
*/
|
||||
interface MessageSubscriberInterface extends MessageHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Return a list of messages to be handled.
|
||||
*
|
||||
* It returns a list of messages like in the following example:
|
||||
*
|
||||
* return [MyMessage::class];
|
||||
*
|
||||
* It can also change the priority per classes.
|
||||
*
|
||||
* return [
|
||||
* [FirstMessage::class, 0],
|
||||
* [SecondMessage::class, -10],
|
||||
* ];
|
||||
*
|
||||
* The `__invoke` method of the handler will be called as usual with the message to handle.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getHandledMessages(): array;
|
||||
}
|
@ -18,7 +18,10 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Symfony\Component\Messenger\ContainerHandlerLocator;
|
||||
use Symfony\Component\Messenger\DependencyInjection\MessengerPass;
|
||||
use Symfony\Component\Messenger\Handler\ChainHandler;
|
||||
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
|
||||
use Symfony\Component\Messenger\Transport\ReceiverInterface;
|
||||
|
||||
class MessengerPassTest extends TestCase
|
||||
@ -50,6 +53,34 @@ class MessengerPassTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetClassesFromTheHandlerSubscriberInterface()
|
||||
{
|
||||
$container = $this->getContainerBuilder();
|
||||
$container
|
||||
->register(HandlerWithMultipleMessages::class, HandlerWithMultipleMessages::class)
|
||||
->addTag('messenger.message_handler')
|
||||
;
|
||||
$container
|
||||
->register(PrioritizedHandler::class, PrioritizedHandler::class)
|
||||
->addTag('messenger.message_handler')
|
||||
;
|
||||
|
||||
(new MessengerPass())->process($container);
|
||||
|
||||
$handlerLocatorDefinition = $container->getDefinition($container->getDefinition('messenger.handler_resolver')->getArgument(0));
|
||||
$handlerMapping = $handlerLocatorDefinition->getArgument(0);
|
||||
|
||||
$this->assertArrayHasKey('handler.'.DummyMessage::class, $handlerMapping);
|
||||
$this->assertEquals(new ServiceClosureArgument(new Reference(HandlerWithMultipleMessages::class)), $handlerMapping['handler.'.DummyMessage::class]);
|
||||
|
||||
$this->assertArrayHasKey('handler.'.SecondMessage::class, $handlerMapping);
|
||||
$handlerReference = (string) $handlerMapping['handler.'.SecondMessage::class]->getValues()[0];
|
||||
$definition = $container->getDefinition($handlerReference);
|
||||
|
||||
$this->assertSame(ChainHandler::class, $definition->getClass());
|
||||
$this->assertEquals(array(new Reference(PrioritizedHandler::class), new Reference(HandlerWithMultipleMessages::class)), $definition->getArgument(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
* @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 @@ class MessengerPassTest extends TestCase
|
||||
(new MessengerPass())->process($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
* @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.
|
||||
*/
|
||||
public function testUndefinedMessageClassForHandlerViaInterface()
|
||||
{
|
||||
$container = $this->getContainerBuilder();
|
||||
$container
|
||||
->register(UndefinedMessageHandlerViaInterface::class, UndefinedMessageHandlerViaInterface::class)
|
||||
->addTag('messenger.message_handler')
|
||||
;
|
||||
|
||||
(new MessengerPass())->process($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
* @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\NotInvokableHandler": class "Symfony\Component\Messenger\Tests\DependencyInjection\NotInvokableHandler" must have an "__invoke()" method.
|
||||
@ -125,6 +171,21 @@ class MessengerPassTest extends TestCase
|
||||
(new MessengerPass())->process($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
* @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\HandleNoMessageHandler": method "Symfony\Component\Messenger\Tests\DependencyInjection\HandleNoMessageHandler::getHandledMessages()" must return one or more messages.
|
||||
*/
|
||||
public function testNeedsToHandleAtLeastOneMessage()
|
||||
{
|
||||
$container = $this->getContainerBuilder();
|
||||
$container
|
||||
->register(HandleNoMessageHandler::class, HandleNoMessageHandler::class)
|
||||
->addTag('messenger.message_handler')
|
||||
;
|
||||
|
||||
(new MessengerPass())->process($container);
|
||||
}
|
||||
|
||||
private function getContainerBuilder(): ContainerBuilder
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
@ -168,6 +229,18 @@ class UndefinedMessageHandler
|
||||
}
|
||||
}
|
||||
|
||||
class UndefinedMessageHandlerViaInterface implements MessageSubscriberInterface
|
||||
{
|
||||
public static function getHandledMessages(): array
|
||||
{
|
||||
return array(UndefinedMessage::class);
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class NotInvokableHandler
|
||||
{
|
||||
}
|
||||
@ -192,3 +265,32 @@ class BuiltinArgumentTypeHandler
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class HandlerWithMultipleMessages implements MessageSubscriberInterface
|
||||
{
|
||||
public static function getHandledMessages(): array
|
||||
{
|
||||
return array(
|
||||
DummyMessage::class,
|
||||
SecondMessage::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PrioritizedHandler implements MessageSubscriberInterface
|
||||
{
|
||||
public static function getHandledMessages(): array
|
||||
{
|
||||
return array(
|
||||
array(SecondMessage::class, 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HandleNoMessageHandler implements MessageSubscriberInterface
|
||||
{
|
||||
public static function getHandledMessages(): array
|
||||
{
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user