diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 9789ec038cf43..230676d8eef88 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -74,14 +74,27 @@ public function process(ContainerBuilder $container) private function registerHandlers(ContainerBuilder $container) { + $definitions = array(); $handlersByMessage = array(); foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) { foreach ($tags as $tag) { - $handles = isset($tag['handles']) ? array($tag['handles']) : $this->guessHandledClasses($r = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()), $serviceId); + $r = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()); + + if (isset($tag['handles'])) { + $handles = isset($tag['method']) ? array($tag['handles'] => $tag['method']) : array($tag['handles']); + } else { + $handles = $this->guessHandledClasses($r, $serviceId); + } + $priority = $tag['priority'] ?? 0; - foreach ($handles as $messageClass) { + foreach ($handles as $messageClass => $method) { + if (\is_int($messageClass)) { + $messageClass = $method; + $method = '__invoke'; + } + if (\is_array($messageClass)) { $messagePriority = $messageClass[1]; $messageClass = $messageClass[0]; @@ -89,12 +102,27 @@ private function registerHandlers(ContainerBuilder $container) $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()); + if (\is_array($method)) { + $messagePriority = $method[1]; + $method = $method[0]; + } + + if (!\class_exists($messageClass)) { + $messageClassLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : $r->implementsInterface(MessageHandlerInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method); throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $messageClass, $messageClassLocation)); } + if (!$r->hasMethod($method)) { + throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method)); + } + + if ('__invoke' !== $method) { + $wrapperDefinition = (new Definition('callable'))->addArgument(array(new Reference($serviceId), $method))->setFactory('Closure::fromCallable'); + + $definitions[$serviceId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($messageClass.':'.$messagePriority.':'.$serviceId.':'.$method)] = $wrapperDefinition; + } + $handlersByMessage[$messageClass][$messagePriority][] = new Reference($serviceId); } } @@ -105,7 +133,6 @@ private function registerHandlers(ContainerBuilder $container) $handlersByMessage[$message] = array_merge(...$handlersByMessage[$message]); } - $definitions = array(); $handlersLocatorMapping = array(); foreach ($handlersByMessage as $message => $handlers) { if (1 === \count($handlers)) { diff --git a/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php b/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php index 6a751482bff0b..ba4ae3ee79870 100644 --- a/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php +++ b/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php @@ -34,9 +34,14 @@ interface MessageSubscriberInterface extends MessageHandlerInterface * [SecondMessage::class, -10], * ]; * - * The `__invoke` method of the handler will be called as usual with the message to handle. + * It can also specify a method and/or a priority per message: * - * @return array + * return [ + * FirstMessage::class => 'firstMessageMethod', + * SecondMessage::class => ['secondMessageMethod', 20], + * ]; + * + * The `__invoke` method of the handler will be called as usual with the message to handle. */ - public static function getHandledMessages(): array; + public static function getHandledMessages(): iterable; } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index eeb5d585f5cda..ac32461da6a27 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -98,6 +98,53 @@ public function testGetClassesFromTheHandlerSubscriberInterface() $this->assertEquals(array(new Reference(PrioritizedHandler::class), new Reference(HandlerWithMultipleMessages::class)), $definition->getArgument(0)); } + public function testGetClassesAndMethodsAndPrioritiesFromTheSubscriber() + { + $container = $this->getContainerBuilder(); + $container + ->register(HandlerMappingMethods::class, HandlerMappingMethods::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->assertArrayHasKey('handler.'.SecondMessage::class, $handlerMapping); + + $dummyHandlerReference = (string) $handlerMapping['handler.'.DummyMessage::class]->getValues()[0]; + $dummyHandlerDefinition = $container->getDefinition($dummyHandlerReference); + $this->assertSame('callable', $dummyHandlerDefinition->getClass()); + $this->assertEquals(array(new Reference(HandlerMappingMethods::class), 'dummyMethod'), $dummyHandlerDefinition->getArgument(0)); + $this->assertSame(array('Closure', 'fromCallable'), $dummyHandlerDefinition->getFactory()); + + $secondHandlerReference = (string) $handlerMapping['handler.'.SecondMessage::class]->getValues()[0]; + $secondHandlerDefinition = $container->getDefinition($secondHandlerReference); + $this->assertSame(ChainHandler::class, $secondHandlerDefinition->getClass()); + $this->assertEquals(new Reference(PrioritizedHandler::class), $secondHandlerDefinition->getArgument(0)[1]); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerMappingWithNonExistentMethod": method "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerMappingWithNonExistentMethod::dummyMethod()" does not exist. + */ + public function testThrowsExceptionIfTheHandlerMethodDoesNotExist() + { + $container = $this->getContainerBuilder(); + $container + ->register(HandlerMappingWithNonExistentMethod::class, HandlerMappingWithNonExistentMethod::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + } + public function testItRegistersReceivers() { $container = $this->getContainerBuilder(); @@ -397,7 +444,7 @@ public function __invoke(UndefinedMessage $message) class UndefinedMessageHandlerViaInterface implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array(UndefinedMessage::class); } @@ -434,31 +481,72 @@ public function __invoke(string $message) class HandlerWithMultipleMessages implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array( DummyMessage::class, SecondMessage::class, ); } + + public function __invoke() + { + } } class PrioritizedHandler implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array( array(SecondMessage::class, 10), ); } + + public function __invoke() + { + } +} + +class HandlerMappingMethods implements MessageSubscriberInterface +{ + public static function getHandledMessages(): iterable + { + return array( + DummyMessage::class => 'dummyMethod', + SecondMessage::class => array('secondMessage', 20), + ); + } + + public function dummyMethod() + { + } + + public function secondMessage() + { + } +} + +class HandlerMappingWithNonExistentMethod implements MessageSubscriberInterface +{ + public static function getHandledMessages(): iterable + { + return array( + DummyMessage::class => 'dummyMethod', + ); + } } class HandleNoMessageHandler implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array(); } + + public function __invoke() + { + } } class UselessMiddleware implements MiddlewareInterface