[Messenger] Add a MessageHandlerInterface (multiple messages + auto-configuration)

This commit is contained in:
Samuel ROZE 2018-03-27 19:04:58 +02:00
parent 5b68f69250
commit 07e6bc73a3
5 changed files with 196 additions and 11 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
{
}

View File

@ -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;
}

View File

@ -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();
}
}