From 07e6bc73a34261b20195f7151c00d992aaaff619 Mon Sep 17 00:00:00 2001 From: Samuel ROZE Date: Tue, 27 Mar 2018 19:04:58 +0200 Subject: [PATCH] [Messenger] Add a `MessageHandlerInterface` (multiple messages + auto-configuration) --- .../FrameworkExtension.php | 3 + .../DependencyInjection/MessengerPass.php | 37 +++++-- .../Handler/MessageHandlerInterface.php | 21 ++++ .../Handler/MessageSubscriberInterface.php | 40 +++++++ .../DependencyInjection/MessengerPassTest.php | 102 ++++++++++++++++++ 5 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/Messenger/Handler/MessageHandlerInterface.php create mode 100644 src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ff7dbeaa5d60..1d37482dea83 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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; @@ -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 diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 7ca1221b66bf..c7b5ad5fadde 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -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 @@ -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); + } } } @@ -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) { @@ -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) diff --git a/src/Symfony/Component/Messenger/Handler/MessageHandlerInterface.php b/src/Symfony/Component/Messenger/Handler/MessageHandlerInterface.php new file mode 100644 index 000000000000..a5a58b81b0ef --- /dev/null +++ b/src/Symfony/Component/Messenger/Handler/MessageHandlerInterface.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +interface MessageHandlerInterface +{ +} diff --git a/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php b/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php new file mode 100644 index 000000000000..ab81318cdd70 --- /dev/null +++ b/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php @@ -0,0 +1,40 @@ + + * + * 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 + */ +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; +} diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index 7daba2519f19..c950f94dbe00 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -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 @@ -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. @@ -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. @@ -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(); @@ -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 { } @@ -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(); + } +}