diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 169a1c0a6909a..d15ca71c76f48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -65,6 +65,7 @@ class UnusedTagsPass implements CompilerPassInterface 'messenger.bus', 'messenger.message_handler', 'messenger.receiver', + 'messenger.routed_message', 'messenger.transport_factory', 'mime.mime_type_guesser', 'monolog.logger', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7292ed0f79f63..6ad0eeb78df54 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -110,6 +110,7 @@ use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mercure\HubRegistry; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +use Symfony\Component\Messenger\Attribute\AsRoutedMessage; use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; @@ -687,6 +688,14 @@ public function load(array $configs, ContainerBuilder $container) } $definition->addTag('messenger.message_handler', $tagAttributes); }); + $container->registerAttributeForAutoconfiguration(AsRoutedMessage::class, static function (ChildDefinition $definition, AsRoutedMessage $attribute, \ReflectionClass $reflector): void { + $tagAttributes = [ + 'class' => $reflector->getName(), + 'transports' => $attribute->getTransports(), + ]; + + $definition->addTag('messenger.routed_message', $tagAttributes); + }); if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers diff --git a/src/Symfony/Component/Messenger/Attribute/AsRoutedMessage.php b/src/Symfony/Component/Messenger/Attribute/AsRoutedMessage.php new file mode 100644 index 0000000000000..7520c2b15ae64 --- /dev/null +++ b/src/Symfony/Component/Messenger/Attribute/AsRoutedMessage.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Attribute; + +/** + * Attribute to configure transports to be used to dispatch a message. + * + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsRoutedMessage +{ + private array $transports; + + public function __construct(array|string $transports) + { + $this->transports = (array) $transports; + } + + public function getTransports(): array + { + return $this->transports; + } +} diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index b60f066320d64..067e84ea900a1 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG `Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport` and `Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory` * Allow passing a string instead of an array in `TransportNamesStamp` + * Add `#[AsRoutedMessage]` attribute 6.2 --- diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index ac2b97fc3df85..19412caa2b5e6 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -56,6 +56,10 @@ public function process(ContainerBuilder $container) $this->registerReceivers($container, $busIds); } $this->registerHandlers($container, $busIds); + + if ($container->hasDefinition('messenger.senders_locator')) { + $this->registerRoutedMessages($container); + } } private function registerHandlers(ContainerBuilder $container, array $busIds): void @@ -405,4 +409,30 @@ private function getServiceClass(ContainerBuilder $container, string $serviceId) return $definition->getClass(); } } + + private function registerRoutedMessages(ContainerBuilder $container): void + { + $mapping = $container->getDefinition('messenger.senders_locator')->getArgument(0) ?? []; + $receiverAliases = array_map( + static fn ($tagAttributes) => $tagAttributes[0]['alias'], + $container->findTaggedServiceIds('messenger.receiver') + ); + + foreach ($container->findTaggedServiceIds('messenger.routed_message') as [$routedMessage]) { + $messageClass = $routedMessage['class']; + $mapping[$routedMessage['class']] ??= []; + + foreach ($routedMessage['transports'] as $transport) { + if (!\in_array($transport, $receiverAliases)) { + throw new RuntimeException(sprintf('Invalid Messenger routing configuration: the "%s" class is being routed to a sender called "%s". This is not a valid transport or service id.', $messageClass, $transport)); + } + + $mapping[$messageClass][] = $transport; + } + } + + $container->getDefinition('messenger.senders_locator') + ->setArgument(0, $mapping) + ; + } } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index a72efcc15e29e..a93bbea98142a 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -25,6 +25,7 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver; +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisReceiver; use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; use Symfony\Component\Messenger\Command\DebugCommand; use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; @@ -918,6 +919,68 @@ public function testFailedCommandsRegisteredWithServiceLocatorArgumentReplaced() $removeDefinition = $container->getDefinition('console.command.messenger_failed_messages_remove'); $this->assertNotNull($removeDefinition->getArgument(1)); } + + public function testRegisterRoutedMessageWithEmptySenderLocator() + { + $container = $this->getContainerBuilder(); + $container->register('messenger.senders_locator', ServiceLocator::class)->setArgument(0, []); + $container->register(AmqpReceiver::class, AmqpReceiver::class)->addTag('messenger.receiver', ['alias' => 'amqp']); + + $container->register(DummyMessage::class) + ->addTag('messenger.routed_message', [ + 'class' => DummyMessage::class, + 'transports' => ['amqp'], + ]) + ; + + (new MessengerPass())->process($container); + $mapping = $container->getDefinition('messenger.senders_locator')->getArgument(0); + + $this->assertArrayHasKey(DummyMessage::class, $mapping); + $this->assertSame($mapping[DummyMessage::class][0], 'amqp'); + } + + public function testRegisterRoutedMessageWithInvalidTransport() + { + $container = $this->getContainerBuilder(); + $container->register('messenger.senders_locator', ServiceLocator::class)->setArgument(0, []); + + $container->register(DummyMessage::class) + ->addTag('messenger.routed_message', [ + 'class' => DummyMessage::class, + 'transports' => ['amqp'], + ]) + ; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Invalid Messenger routing configuration: the "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" class is being routed to a sender called "amqp". This is not a valid transport or service id.'); + (new MessengerPass())->process($container); + } + + public function testRegisterRoutedMessageWithExistingSenders() + { + $container = $this->getContainerBuilder(); + $container->register('messenger.senders_locator', ServiceLocator::class)->setArgument(0, [ + DummyMessage::class => ['amqp'], + ]); + + $container->register(AmqpReceiver::class, AmqpReceiver::class)->addTag('messenger.receiver', ['alias' => 'amqp']); + $container->register(RedisReceiver::class, RedisReceiver::class)->addTag('messenger.receiver', ['alias' => 'redis']); + + $container->register(DummyMessage::class) + ->addTag('messenger.routed_message', [ + 'class' => DummyMessage::class, + 'transports' => ['redis'], + ]) + ; + + (new MessengerPass())->process($container); + $mapping = $container->getDefinition('messenger.senders_locator')->getArgument(0); + + $this->assertArrayHasKey(DummyMessage::class, $mapping); + $this->assertSame($mapping[DummyMessage::class][0], 'amqp'); + $this->assertSame($mapping[DummyMessage::class][1], 'redis'); + } } class DummyHandler