diff --git a/src/Symfony/Component/Messenger/Attribute/Transport.php b/src/Symfony/Component/Messenger/Attribute/Transport.php new file mode 100644 index 0000000000000..a5da179ec44e3 --- /dev/null +++ b/src/Symfony/Component/Messenger/Attribute/Transport.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Attribute; + +/** + * @author Maxim Dovydenok + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] +class Transport +{ + public function __construct(public readonly string $name) + { + } +} diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 98193ec5c02c4..3409acc041e13 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `SerializedMessageStamp` to avoid serializing a message when a retry occurs. * Automatically resolve handled message type when method different from `__invoke` is used as handler. + * Add `Transport` attribute to configure message to sender mapping 6.0 --- diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageInterfaceWithAttribute.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageInterfaceWithAttribute.php new file mode 100644 index 0000000000000..0f9a78b34072a --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageInterfaceWithAttribute.php @@ -0,0 +1,10 @@ +message = $message; + } + + public function getMessage(): string + { + return $this->message; + } +} + +#[\Attribute(\Attribute::TARGET_CLASS)] +class CustomTransport extends Transport +{ + public function __construct() + { + parent::__construct('message_attribute_sender_2'); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageWithAttributeAndInterface.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageWithAttributeAndInterface.php new file mode 100644 index 0000000000000..cb447a6a9b4a4 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageWithAttributeAndInterface.php @@ -0,0 +1,21 @@ +message = $message; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php index db4cb0efa0782..f3ee2ab3a71ed 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php @@ -15,12 +15,72 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageInterfaceWithAttribute; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageWithAttribute; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageWithAttributeAndInterface; use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage; use Symfony\Component\Messenger\Transport\Sender\SenderInterface; use Symfony\Component\Messenger\Transport\Sender\SendersLocator; class SendersLocatorTest extends TestCase { + public function testAttributeMapping() + { + $sender = $this->createMock(SenderInterface::class); + $sendersLocator = $this->createContainer([ + 'message_attribute_sender' => $sender, + 'message_attribute_sender_2' => $sender, + 'interface_attribute_sender' => $sender, + 'message_config_sender' => $sender, + 'interface_config_sender' => $sender, + 'all_config_sender' => $sender, + ]); + + $locator = new SendersLocator([], $sendersLocator); + $this->assertSame([], iterator_to_array($locator->getSenders(new Envelope(new DummyMessage('a'))))); + $this->assertSame( + ['message_attribute_sender' => $sender, 'message_attribute_sender_2' => $sender], + iterator_to_array($locator->getSenders(new Envelope(new DummyMessageWithAttribute('a')))) + ); + $this->assertSame( + ['message_attribute_sender' => $sender, 'interface_attribute_sender' => $sender], + iterator_to_array($locator->getSenders(new Envelope(new DummyMessageWithAttributeAndInterface('a')))) + ); + + $locatorWithFullRouting = new SendersLocator([ + DummyMessageWithAttribute::class => ['message_config_sender'], + DummyMessageWithAttributeAndInterface::class => ['message_config_sender'], + DummyMessageInterfaceWithAttribute::class => ['interface_config_sender'], + '*' => ['all_config_sender'], + ], $sendersLocator); + $this->assertSame( + ['message_config_sender' => $sender, 'all_config_sender' => $sender], + iterator_to_array($locatorWithFullRouting->getSenders(new Envelope(new DummyMessageWithAttribute('a')))) + ); + $this->assertSame( + ['message_config_sender' => $sender, 'interface_config_sender' => $sender, 'all_config_sender' => $sender], + iterator_to_array($locatorWithFullRouting->getSenders(new Envelope(new DummyMessageWithAttributeAndInterface('a')))) + ); + + $locatorWithClassRouting = new SendersLocator([ + DummyMessageWithAttributeAndInterface::class => ['message_config_sender'], + '*' => ['all_config_sender'], + ], $sendersLocator); + $this->assertSame( + ['message_config_sender' => $sender, 'interface_attribute_sender' => $sender, 'all_config_sender' => $sender], + iterator_to_array($locatorWithClassRouting->getSenders(new Envelope(new DummyMessageWithAttributeAndInterface('a')))) + ); + + $locatorWithInterfaceRouting = new SendersLocator([ + DummyMessageInterfaceWithAttribute::class => ['interface_config_sender'], + '*' => ['all_config_sender'], + ], $sendersLocator); + $this->assertSame( + ['message_attribute_sender' => $sender, 'interface_config_sender' => $sender, 'all_config_sender' => $sender], + iterator_to_array($locatorWithInterfaceRouting->getSenders(new Envelope(new DummyMessageWithAttributeAndInterface('a')))) + ); + } + public function testItReturnsTheSenderBasedOnTheMessageClass() { $sender = $this->createMock(SenderInterface::class); diff --git a/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php b/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php index cda20008c8768..58b9be48a03c0 100644 --- a/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php +++ b/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Transport\Sender; use Psr\Container\ContainerInterface; +use Symfony\Component\Messenger\Attribute\Transport; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\RuntimeException; use Symfony\Component\Messenger\Handler\HandlersLocator; @@ -41,20 +42,56 @@ public function __construct(array $sendersMap, ContainerInterface $sendersLocato */ public function getSenders(Envelope $envelope): iterable { - $seen = []; + $senderAliases = []; foreach (HandlersLocator::listTypes($envelope) as $type) { + $typeSenderAliases = []; + foreach ($this->sendersMap[$type] ?? [] as $senderAlias) { - if (!\in_array($senderAlias, $seen, true)) { - if (!$this->sendersLocator->has($senderAlias)) { - throw new RuntimeException(sprintf('Invalid senders configuration: sender "%s" is not in the senders locator.', $senderAlias)); - } - - $seen[] = $senderAlias; - $sender = $this->sendersLocator->get($senderAlias); - yield $senderAlias => $sender; - } + $typeSenderAliases[] = $senderAlias; + } + + if (!$typeSenderAliases) { + $typeSenderAliases = $this->getSendersFromAttributes($type); + } + + $senderAliases = array_merge($senderAliases, $typeSenderAliases); + } + + $senderAliases = array_unique($senderAliases); + + foreach ($senderAliases as $senderAlias) { + if (!$this->sendersLocator->has($senderAlias)) { + throw new RuntimeException(sprintf('Invalid senders configuration: sender "%s" is not in the senders locator.', $senderAlias)); } + + $sender = $this->sendersLocator->get($senderAlias); + yield $senderAlias => $sender; + } + } + + /** + * @return string[] + */ + private function getSendersFromAttributes(string $type): array + { + if (!class_exists($type) && !interface_exists($type)) { + return []; } + + try { + $reflectionClass = new \ReflectionClass($type); + } catch (\ReflectionException $e) { + return []; + } + + $attributes = $reflectionClass->getAttributes(Transport::class, \ReflectionAttribute::IS_INSTANCEOF); + + return array_map(function (\ReflectionAttribute $attribute): string { + /** @var Transport $attributeInstance */ + $attributeInstance = $attribute->newInstance(); + + return $attributeInstance->name; + }, $attributes); } }