Skip to content

Commit

Permalink
feature #26685 [Messenger] Add a MessageHandlerInterface (multiple …
Browse files Browse the repository at this point in the history
…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: #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
-------

07e6bc7 [Messenger] Add a `MessageHandlerInterface` (multiple messages + auto-configuration)
  • Loading branch information
sroze committed Apr 3, 2018
2 parents 56cd3d2 + 07e6bc7 commit d5b88eb
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 9 deletions.
Expand Up @@ -60,6 +60,7 @@
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;
Expand Down Expand Up @@ -347,6 +348,8 @@ public function load(array $configs, ContainerBuilder $container)
->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
Expand Down
Expand Up @@ -19,6 +19,8 @@
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>
Expand Down Expand Up @@ -67,16 +69,25 @@ private function registerHandlers(ContainerBuilder $container)

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);
$handles = $tag['handles'] ?? $this->guessHandledClasses($r = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()), $serviceId);
$priority = $tag['priority'] ?? 0;

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());
foreach ($handles as $messageClass) {
if (is_array($messageClass)) {
$messagePriority = $messageClass[1];
$messageClass = $messageClass[0];
} else {
$messagePriority = $priority;
}

throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $handles, $messageClassLocation));
}
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());

$priority = $tag['priority'] ?? 0;
$handlersByMessage[$handles][$priority][] = new Reference($serviceId);
throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $messageClass, $messageClassLocation));
}

$handlersByMessage[$messageClass][$messagePriority][] = new Reference($serviceId);
}
}
}

Expand Down Expand Up @@ -108,8 +119,16 @@ private function registerHandlers(ContainerBuilder $container)
$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) {
Expand All @@ -129,7 +148,7 @@ private function guessHandledClass(\ReflectionClass $handlerClass, string $servi
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)
Expand Down
@@ -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;
}
Expand Up @@ -18,7 +18,10 @@
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
Expand Down Expand Up @@ -50,6 +53,34 @@ public function testProcess()
);
}

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.
Expand All @@ -65,6 +96,21 @@ public function testUndefinedMessageClassForHandler()
(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.
Expand Down Expand Up @@ -125,6 +171,21 @@ public function testBuiltinArgumentTypeHandler()
(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();
Expand Down Expand Up @@ -168,6 +229,18 @@ public function __invoke(UndefinedMessage $message)
}
}

class UndefinedMessageHandlerViaInterface implements MessageSubscriberInterface
{
public static function getHandledMessages(): array
{
return array(UndefinedMessage::class);
}

public function __invoke()
{
}
}

class NotInvokableHandler
{
}
Expand All @@ -192,3 +265,32 @@ public function __invoke(string $message)
{
}
}

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

0 comments on commit d5b88eb

Please sign in to comment.