Skip to content

Commit a826eb3

Browse files
feature #48531 [FrameworkBundle][Messenger] Add support for namespace wildcard in Messenger routing (brzuchal)
This PR was merged into the 6.3 branch. Discussion ---------- [FrameworkBundle][Messenger] Add support for namespace wildcard in Messenger routing | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT | Doc PR | symfony/symfony-docs#17532 This is a new feature for the Messenger component which is why there is no issue ticket attached. Currently, HandlersLocator extracts message class along with all parent classes and all interfaces + a `'*'` wildcard to match any transport. This is not fully efficient resulting in many routes for messages that share the same transport but by design don't share any inheritance or interface. This is an opportunity to introduce simple wildcards which allow defining a transport for multiple messages grouped in one namespace. For eg. given a bunch of messages acting as commands, queries, and events (in CQRS) * `App\Message\Command\CreatePost` * `App\Message\Command\PublishPost` * `App\Message\Event\PostCreated` * `App\Message\Event\PostPublished` * `App\Message\Query\FindPost` The intention may be to send all Commands to `async` transport, Queries to `sync` transport but Events to a different transport for eg. `stream` - the configuration requires creating routing for at least 3 classes because none of them share inheritance or interfaces with one wildcard `'*'` for `async` or `sync` as these are the most numerous. ```yaml framework: messenger: routing: App\Message\Command\CreatePost: async App\Message\Command\PublishPost: async App\Message\Query\FindPost: sync '*': stream ``` With this feature managing, multiple types of transport allow us to ultimately define 3 wildcarded namespaces instead and reduce the maintenance burden at the same time and prevent using standalone `'*'` which unconsciously may be the cause of problems. The configuration using namespace wildcards will look as follows: ```yaml framework: messenger: routing: App\Message\Command\*: async App\Message\Event\*: stream App\Message\Query\*: sync # '*': stream ``` The aim of this PR is to provide a simple solution supporting wildcards in routing to prevent using `'*'`. In future scope, matching rules may be the subject of another PR addressing improvements in the future. Commits ------- c352f53 [FrameworkBundle][Messenger] Add support for namespace wildcard in Messenger routing
2 parents fcfca3e + c352f53 commit a826eb3

File tree

12 files changed

+101
-1
lines changed

12 files changed

+101
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `DomCrawlerAssertionsTrait::assertSelectorCount(int $count, string $selector)`
88
* Allow to avoid `limit` definition in a RateLimiter configuration when using the `no_limit` policy
99
* Add `--format` option to the `debug:config` command
10+
* Add support to pass namespace wildcard in `framework.messenger.routing`
1011

1112
6.2
1213
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,11 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
21592159

21602160
$messageToSendersMapping = [];
21612161
foreach ($config['routing'] as $message => $messageConfiguration) {
2162-
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
2162+
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false) && !preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++\*$/', $message)) {
2163+
if (str_contains($message, '*')) {
2164+
throw new LogicException(sprintf('Invalid Messenger routing configuration: invalid namespace "%s" wildcard.', $message));
2165+
}
2166+
21632167
throw new LogicException(sprintf('Invalid Messenger routing configuration: class or interface "%s" not found.', $message));
21642168
}
21652169

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
SecondMessage::class => [
1616
'senders' => ['amqp', 'audit'],
1717
],
18+
'Symfony\*' => 'amqp',
1819
'*' => 'amqp',
1920
],
2021
'transports' => [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
$container->loadFromExtension('framework', [
4+
'http_method_override' => false,
5+
'serializer' => true,
6+
'messenger' => [
7+
'serializer' => [
8+
'default_serializer' => 'messenger.transport.symfony_serializer',
9+
],
10+
'routing' => [
11+
'Symfony\*\DummyMessage' => ['audit'],
12+
],
13+
'transports' => [
14+
'audit' => 'null://',
15+
],
16+
],
17+
]);

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
<framework:routing message-class="*">
2121
<framework:sender service="amqp" />
2222
</framework:routing>
23+
<framework:routing message-class="Symfony\*">
24+
<framework:sender service="amqp" />
25+
</framework:routing>
2326
<framework:transport name="amqp" dsn="amqp://localhost/%2f/messages" />
2427
<framework:transport name="audit" dsn="null://" />
2528
</framework:messenger>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:framework="http://symfony.com/schema/dic/symfony"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
6+
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
7+
8+
<framework:config http-method-override="false">
9+
<framework:serializer enabled="true" />
10+
<framework:messenger>
11+
<framework:serializer default-serializer="messenger.transport.symfony_serializer" />
12+
<framework:routing message-class="Symfony\*\DummyMessage">
13+
<framework:sender service="audit" />
14+
</framework:routing>
15+
<framework:transport name="audit" dsn="null://" />
16+
</framework:messenger>
17+
</framework:config>
18+
</container>

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ framework:
88
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': [amqp, messenger.transport.audit]
99
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage':
1010
senders: [amqp, audit]
11+
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\*': [amqp]
12+
'Symfony\*': [amqp]
1113
'*': amqp
1214
transports:
1315
amqp: 'amqp://localhost/%2f/messages'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
framework:
2+
http_method_override: false
3+
serializer: true
4+
messenger:
5+
serializer:
6+
default_serializer: messenger.transport.symfony_serializer
7+
routing:
8+
'Symfony\*\DummyMessage': [audit]
9+
transports:
10+
audit: 'null://'

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,13 @@ public function testMessengerMiddlewareFactoryErroneousFormat()
10591059
}
10601060

10611061
public function testMessengerInvalidTransportRouting()
1062+
{
1063+
$this->expectException(\LogicException::class);
1064+
$this->expectExceptionMessage('Invalid Messenger routing configuration: invalid namespace "Symfony\*\DummyMessage" wildcard.');
1065+
$this->createContainerFromFile('messenger_routing_invalid_wildcard');
1066+
}
1067+
1068+
public function testMessengerInvalidWildcardRouting()
10621069
{
10631070
$this->expectException(\LogicException::class);
10641071
$this->expectExceptionMessage('Invalid Messenger routing configuration: the "Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage" class is being routed to a sender called "invalid". This is not a valid transport or service id.');

src/Symfony/Component/Messenger/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.3
55
---
66

7+
* Add support for namespace wildcards in the HandlersLocator to allow routing multiple messages within the same namespace
78
* Deprecate `Symfony\Component\Messenger\Transport\InMemoryTransport` and
89
`Symfony\Component\Messenger\Transport\InMemoryTransportFactory` in favor of
910
`Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport` and

src/Symfony/Component/Messenger/Handler/HandlersLocator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,22 @@ public static function listTypes(Envelope $envelope): array
6868
return [$class => $class]
6969
+ class_parents($class)
7070
+ class_implements($class)
71+
+ self::listWildcards($class)
7172
+ ['*' => '*'];
7273
}
7374

75+
private static function listWildcards(string $type): array
76+
{
77+
$type .= '\*';
78+
$wildcards = [];
79+
while ($i = strrpos($type, '\\', -3)) {
80+
$type = substr_replace($type, '\*', $i);
81+
$wildcards[$type] = $type;
82+
}
83+
84+
return $wildcards;
85+
}
86+
7487
private function shouldHandle(Envelope $envelope, HandlerDescriptor $handlerDescriptor): bool
7588
{
7689
if (null === $received = $envelope->last(ReceivedStamp::class)) {

src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ public function testItReturnsOnlyHandlersMatchingTransport()
5656
new Envelope(new DummyMessage('Body'), [new ReceivedStamp('transportName')])
5757
)));
5858
}
59+
60+
public function testItReturnsOnlyHandlersMatchingMessageNamespace()
61+
{
62+
$firstHandler = $this->createPartialMock(HandlersLocatorTestCallable::class, ['__invoke']);
63+
$secondHandler = $this->createPartialMock(HandlersLocatorTestCallable::class, ['__invoke']);
64+
65+
$locator = new HandlersLocator([
66+
str_replace('DummyMessage', '*', DummyMessage::class) => [
67+
$first = new HandlerDescriptor($firstHandler, ['alias' => 'one']),
68+
],
69+
str_replace('Fixtures\\DummyMessage', '*', DummyMessage::class) => [
70+
$second = new HandlerDescriptor($secondHandler, ['alias' => 'two']),
71+
],
72+
]);
73+
74+
$first->getName();
75+
$second->getName();
76+
77+
$this->assertEquals([
78+
$first,
79+
$second,
80+
], iterator_to_array($locator->getHandlers(new Envelope(new DummyMessage('Body')))));
81+
}
5982
}
6083

6184
class HandlersLocatorTestCallable

0 commit comments

Comments
 (0)