diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 006ffa2d0d5d..7ca1221b66bf 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -67,12 +67,12 @@ private function registerHandlers(ContainerBuilder $container) foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) { foreach ($tags as $tag) { - $handles = $tag['handles'] ?? $this->guessHandledClass($container, $serviceId); + $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"' : 'declared in `__invoke` function'; + $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('The message class "%s" %s of service "%s" does not exist.', $messageClassLocation, $handles, $serviceId)); + throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $handles, $messageClassLocation)); } $priority = $tag['priority'] ?? 0; @@ -108,27 +108,28 @@ private function registerHandlers(ContainerBuilder $container) $handlerResolver->replaceArgument(0, ServiceLocatorTagPass::register($container, $handlersLocatorMapping)); } - private function guessHandledClass(ContainerBuilder $container, string $serviceId): string + private function guessHandledClass(\ReflectionClass $handlerClass, string $serviceId): string { - $reflection = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()); - try { - $method = $reflection->getMethod('__invoke'); + $method = $handlerClass->getMethod('__invoke'); } catch (\ReflectionException $e) { - throw new RuntimeException(sprintf('Service "%s" should have an `__invoke` function.', $serviceId)); + throw new RuntimeException(sprintf('Invalid handler service "%s": class "%s" must have an "__invoke()" method.', $serviceId, $handlerClass->getName())); } $parameters = $method->getParameters(); if (1 !== count($parameters)) { - throw new RuntimeException(sprintf('`__invoke` function of service "%s" must have exactly one parameter.', $serviceId)); + throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::__invoke()" must have exactly one argument corresponding to the message it handles.', $serviceId, $handlerClass->getName())); + } + + if (!$type = $parameters[0]->getType()) { + throw new RuntimeException(sprintf('Invalid handler service "%s": argument "$%s" of method "%s::__invoke()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName())); } - $parameter = $parameters[0]; - if (null === $parameter->getClass()) { - throw new RuntimeException(sprintf('The parameter of `__invoke` function of service "%s" must type hint the message class it handles.', $serviceId)); + if ($type->isBuiltin()) { + 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 $parameter->getClass()->getName(); + return $parameters[0]->getType(); } private function registerReceivers(ContainerBuilder $container) diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php new file mode 100644 index 000000000000..7daba2519f19 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +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\Tests\Fixtures\DummyMessage; +use Symfony\Component\Messenger\Transport\ReceiverInterface; + +class MessengerPassTest extends TestCase +{ + public function testProcess() + { + $container = $this->getContainerBuilder(); + $container + ->register(DummyHandler::class, DummyHandler::class) + ->addTag('messenger.message_handler') + ; + $container + ->register(DummyReceiver::class, DummyReceiver::class) + ->addTag('messenger.receiver') + ; + + (new MessengerPass())->process($container); + + $handlerLocatorDefinition = $container->getDefinition($container->getDefinition('messenger.handler_resolver')->getArgument(0)); + $this->assertSame(ServiceLocator::class, $handlerLocatorDefinition->getClass()); + $this->assertEquals( + array('handler.'.DummyMessage::class => new ServiceClosureArgument(new Reference(DummyHandler::class))), + $handlerLocatorDefinition->getArgument(0) + ); + + $this->assertEquals( + array(DummyReceiver::class => new Reference(DummyReceiver::class)), + $container->getDefinition('messenger.receiver_locator')->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. + */ + public function testUndefinedMessageClassForHandler() + { + $container = $this->getContainerBuilder(); + $container + ->register(UndefinedMessageHandler::class, UndefinedMessageHandler::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. + */ + public function testNotInvokableHandler() + { + $container = $this->getContainerBuilder(); + $container + ->register(NotInvokableHandler::class, NotInvokableHandler::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\MissingArgumentHandler": method "Symfony\Component\Messenger\Tests\DependencyInjection\MissingArgumentHandler::__invoke()" must have exactly one argument corresponding to the message it handles. + */ + public function testMissingArgumentHandler() + { + $container = $this->getContainerBuilder(); + $container + ->register(MissingArgumentHandler::class, MissingArgumentHandler::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\MissingArgumentTypeHandler": argument "$message" of method "Symfony\Component\Messenger\Tests\DependencyInjection\MissingArgumentTypeHandler::__invoke()" must have a type-hint corresponding to the message class it handles. + */ + public function testMissingArgumentTypeHandler() + { + $container = $this->getContainerBuilder(); + $container + ->register(MissingArgumentTypeHandler::class, MissingArgumentTypeHandler::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\BuiltinArgumentTypeHandler": type-hint of argument "$message" in method "Symfony\Component\Messenger\Tests\DependencyInjection\BuiltinArgumentTypeHandler::__invoke()" must be a class , "string" given. + */ + public function testBuiltinArgumentTypeHandler() + { + $container = $this->getContainerBuilder(); + $container + ->register(BuiltinArgumentTypeHandler::class, BuiltinArgumentTypeHandler::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + } + + private function getContainerBuilder(): ContainerBuilder + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + $container->register('message_bus', ContainerHandlerLocator::class); + + $container + ->register('messenger.handler_resolver', ContainerHandlerLocator::class) + ->addArgument(new Reference('service_container')) + ; + + $container->register('messenger.receiver_locator', ServiceLocator::class) + ->addArgument(new Reference('service_container')) + ; + + return $container; + } +} + +class DummyHandler +{ + public function __invoke(DummyMessage $message): void + { + } +} + +class DummyReceiver implements ReceiverInterface +{ + public function receive(): iterable + { + for ($i = 0; $i < 3; ++$i) { + yield new DummyMessage("Dummy $i"); + } + } +} + +class UndefinedMessageHandler +{ + public function __invoke(UndefinedMessage $message) + { + } +} + +class NotInvokableHandler +{ +} + +class MissingArgumentHandler +{ + public function __invoke() + { + } +} + +class MissingArgumentTypeHandler +{ + public function __invoke($message) + { + } +} + +class BuiltinArgumentTypeHandler +{ + public function __invoke(string $message) + { + } +} diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index 7f7d982dc255..c4602ed9e960 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "symfony/serializer": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4.6|~4.0", "symfony/property-access": "~3.4|~4.0" }, "suggest": {