Skip to content

[Messenger] Add Transport attribute to configure message to transport mapping #41179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/Symfony/Component/Messenger/Attribute/Transport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <dovydenok.maxim@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class Transport
{
public function __construct(public readonly string $name)
{
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\Messenger\Tests\Fixtures;

use Symfony\Component\Messenger\Attribute\Transport;

#[Transport('interface_attribute_sender')]
interface DummyMessageInterfaceWithAttribute
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Symfony\Component\Messenger\Tests\Fixtures;

use Symfony\Component\Messenger\Attribute\Transport;

#[Transport('message_attribute_sender')]
#[CustomTransport]
class DummyMessageWithAttribute implements DummyMessageInterface
{
private $message;

public function __construct(string $message)
{
$this->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');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Symfony\Component\Messenger\Tests\Fixtures;

use Symfony\Component\Messenger\Attribute\Transport;

#[Transport('message_attribute_sender')]
class DummyMessageWithAttributeAndInterface implements DummyMessageInterfaceWithAttribute
{
private $message;

public function __construct(string $message)
{
$this->message = $message;
}

public function getMessage(): string
{
return $this->message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
57 changes: 47 additions & 10 deletions src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand it correctly, IF even one real route exists for the message (something in $this->sendersMap[$type]), then we do NOT also add the #[Transport] senders? So basically any config would override the attribute?

I think that is... probably the correct behavior. But what about the * catch-all route in messenger.yaml (or an interface)? I'm wondering if someone will have the use-case where they generally use the attribute, but then they decide to, in addition to the transport in each class, route all messages (or all messages for an interface) to a 2nd transport via the routing config in messenger.yaml.

So, I'm conflicted on this point.

}

$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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate to need to do this at runtime (and not compile time). But as messages are not services... or tagged in any way, there is no way for us to collect the attribute information at container build time. So 👍 for this approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed unfortunate. Any clever ideas? I understand it depends, but is there a non-negligible performance impact to processing the attributes at runtime?

}
}