8000 feature #29166 [Messenger] Add handled & sent stamps (ogizanagi) · symfony/symfony@88891d5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 88891d5

Browse files
feature #29166 [Messenger] Add handled & sent stamps (ogizanagi)
This PR was merged into the 4.2-dev branch. Discussion ---------- [Messenger] Add handled & sent stamps | Q | A | ------------- | --- | Branch? | 4.2 <!-- see below --> | Bug fix? | no | New feature? | yes <!-- don't forget to update src/**/CHANGELOG.md files --> | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tests pass? | yes <!-- please add some, will be required by reviewers --> | Fixed tickets | N/A <!-- #-prefixed issue number(s), if any --> | License | MIT | Doc PR | symfony/symfony-docs/issues/10661 Based on #29159 This new feature marks sent and handled messages, so middleware can act upon these and use the handler(s) result(s). This is also the base of a next PR (#29167), introducing a query bus built on top of the message bus. I'm not sure yet about the best way to determine the handlers and senders names/descriptions to store in the stamps: - Handlers are callable. I've just reused the [console text descriptor](https://github.com/nicolas-grekas/symfony/blob/1c1818b87675d077808dbf7e05da84c2e1ddc9f8/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php#L457-L491) format for now. - ~~Sender are `SenderInterface` instances. `\get_class` is used for now, but a single message can be sent by multiple senders, including of the same class.~~ => Updated. Yielding the sender name if provided, the FQCN otherwise. ~~Instead, what about allowing to yield names from locators, and fallback on the above strategies otherwise? So we'll use transport names from the config for senders, and pre-computed compile-time handlers descriptions?~~ => Done. For handlers, computing it at compile time might not be straightforward. Let's compute it lazily from `HandledStamp::fromCallable()` --- ### From previous conversations: > What about not adding HandledStamp on `null` returned from handler IMHO, `null` still is a result. The stamps allows to identify a message as being handled regardless of the returned value, so makes sense on its own and keeping would require one less check for those wanting to consume it. > What about adding SentStamp? Makes sense to me and I think it was requested by @Nyholm before on Slack. So, included in this PR. > Should it target 4.2 or 4.3? Targeting 4.2, because of the removal of the handler result forwarding by middleware. A userland middleware could have used this result, typically a cache middleware. Which would now require extra boring code in userland. This will simplify it and allow users to create their query bus instance until 4.3. Commits ------- 2f5acf7 [Messenger] Add handled & sent stamps
2 parents 100f205 + 2f5acf7 commit 88891d5

16 files changed

+325
-16
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,9 +1578,10 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
15781578
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
15791579
throw new LogicException(sprintf('Invalid Messenger routing configuration: class or interface "%s" not found.', $message));
15801580
}
1581-
$senders = array_map(function ($sender) use ($senderAliases) {
1582-
return new Reference($senderAliases[$sender] ?? $sender);
1583-
}, $messageConfiguration['senders']);
1581+
$senders = array();
1582+
foreach ($messageConfiguration['senders'] as $sender) {
1583+
$senders[$sender] = new Reference($senderAliases[$sender] ?? $sender);
1584+
}
15841585

15851586
$sendersId = 'messenger.senders.'.$message;
15861587
$container->register($sendersId, RewindableGenerator::class)

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,10 @@ public function testMessengerRouting()
569569
);
570570

571571
$this->assertSame($messageToSendAndHandleMapping, $senderLocatorDefinition->getArgument(1));
572-
$this->assertEquals(array(new Reference('messenger.transport.amqp'), new Reference('audit')), $container->getDefinition('messenger.senders.'.DummyMessage::class)->getArgument(0)[0]->getValues());
572+
$this->assertEquals(array(
573+
'amqp' => new Reference('messenger.transport.amqp'),
574+
'audit' => new Reference('audit'),
575+
), $container->getDefinition('messenger.senders.'.DummyMessage::class)->getArgument(0)[0]->getValues());
573576
}
574577

575578
/**

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
4.2.0
55
-----
66

7+
* Added `HandledStamp` & `SentStamp` stamps
78
* All the changes below are BC BREAKS
89
* Senders and handlers subscribing to parent interfaces now receive *all* matching messages, wildcard included
910
* `MessageBusInterface::dispatch()`, `MiddlewareInterface::handle()` and `SenderInterface::send()` return `Envelope`

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ public function getHandlers(Envelope $envelope): iterable
4040
$seen = array();
4141

4242
foreach (self::listTypes($envelope) as $type) {
43-
foreach ($this->handlers[$type] ?? array() as $handler) {
43+
foreach ($this->handlers[$type] ?? array() as $alias => $handler) {
4444
if (!\in_array($handler, $seen, true)) {
45-
yield $seen[] = $handler;
45+
yield $alias => $seen[] = $handler;
4646
}
4747
}
4848
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface HandlersLocatorInterface
2525
/**
2626
* Returns the handlers for the given message name.
2727
*
28-
* @return iterable|callable[]
28+
* @return iterable|callable[] Indexed by handler alias if available
2929
*/
3030
public function getHandlers(Envelope $envelope): iterable;
3131
}

src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Messenger\Envelope;
1515
use Symfony\Component\Messenger\Exception\NoHandlerForMessageException;
1616
use Symfony\Component\Messenger\Handler\HandlersLocatorInterface;
17+
use Symfony\Component\Messenger\Stamp\HandledStamp;
1718

1819
/**
1920
* @author Samuel Roze <samuel.roze@gmail.com>
@@ -40,8 +41,8 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
4041
{
4142
$handler = null;
4243
$message = $envelope->getMessage();
43-
foreach ($this->handlersLocator->getHandlers($envelope) as $handler) {
44-
$handler($message);
44+
foreach ($this->handlersLocator->getHandlers($envelope) as $alias => $handler) {
45+
$envelope = $envelope->with(HandledStamp::fromCallable($handler, $handler($message), \is_string($alias) ? $alias : null));
4546
}
4647
if (null === $handler && !$this->allowNoHandlers) {
4748
throw new NoHandlerForMessageException(sprintf('No handler for message "%s".', \get_class($envelope->getMessage())));

src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Messenger\Envelope;
1515
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
16+
use Symfony\Component\Messenger\Stamp\SentStamp;
1617
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
1718

1819
/**
@@ -42,8 +43,8 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
4243
$handle = false;
4344
$sender = null;
4445

45-
foreach ($this->sendersLocator->getSenders($envelope, $handle) as $sender) {
46-
$envelope = $sender->send($envelope);
46+
foreach ($this->sendersLocator->getSenders($envelope, $handle) as $alias => $sender) {
47+
$envelope = $sender->send($envelope)->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null));
4748
}
4849

4950
if (null === $sender || $handle) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Messenger\Stamp;
13+
14+
/**
15+
* Stamp identifying a message handled by the `HandleMessageMiddleware` middleware
16+
* and storing the handler returned value.
17+
*
18+
* @see \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware
19+
*
20+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
21+
*
22+
* @experimental in 4.2
23+
*/
24+
final class HandledStamp implements StampInterface
25+
{
26+
private $result;
27+
private $callableName;
28+
private $handlerAlias;
29+
30+
/**
31+
* @param mixed $result The returned value of the message handler
32+
*/
33+
public function __construct($result, string $callableName, string $handlerAlias = null)
34+
{
35+
$this->result = $result;
36+
$this->callableName = $callableName;
37+
$this->handlerAlias = $handlerAlias;
38+
}
39+
40+
/**
41+
* @param mixed $result The returned value of the message handler
42+
*/
43+
public static function fromCallable(callable $handler, $result, string $handlerAlias = null): self
44+
{
45+
if (\is_array($handler)) {
46+
if (\is_object($handler[0])) {
47+
return new self($result, \get_class($handler[0]).'::'.$handler[1], $handlerAlias);
48+
}
49+
50+
return new self($result, $handler[0].'::'.$handler[1], $handlerAlias);
51+
}
52+
53+
if (\is_string($handler)) {
54+
return new self($result, $handler, $handlerAlias);
55+
}
56+
57+
if ($handler instanceof \Closure) {
58+
$r = new \ReflectionFunction($handler);
59+
if (false !== strpos($r->name, '{closure}')) {
60+
return new self($result, 'Closure', $handlerAlias);
61+
}
62+
if ($class = $r->getClosureScopeClass()) {
63+
return new self($result, $class->name.'::'.$r->name, $handlerAlias);
64+
}
65+
66+
return new self($result, $r->name, $handlerAlias);
67+
}
68+
69+
return new self($result, \get_class($handler).'::__invoke', $handlerAlias);
70+
}
71+
72+
/**
73+
* @return mixed
74+
*/
75+
public function getResult()
76+
{
77+
return $this->result;
78+
}
79+
80+
public function getCallableName(): string
81+
{
82+
return $this->callableName;
83+
}
84+
85+
public function getHandlerAlias(): ?string
86+
{
87+
return $this->handlerAlias;
88+
}
89+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Messenger\Stamp;
13+
14+
/**
15+
* Marker stamp identifying a message sent by the `SendMessageMiddleware`.
16+
*
17+
* @see \Symfony\Component\Messenger\Middleware\SendMessageMiddleware
18+
*
19+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
20+
*
21+
* @experimental in 4.2
22+
*/
23+
final class SentStamp implements StampInterface
24+
{
25+
private $senderClass;
26+
private $senderAlias;
27+
28+
public function __construct(string $senderClass, string $senderAlias = null)
29+
{
30+
$this->senderAlias = $senderAlias;
31+
$this->senderClass = $senderClass;
32+
}
33+
34+
public function getSenderClass(): string
35+
{
36+
return $this->senderClass;
37+
}
38+
39+
public function getSenderAlias(): ?string
40+
{
41+
return $this->senderAlias;
42+
}
43+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Messenger\Tests\Handler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Messenger\Envelope;
16+
use Symfony\Component\Messenger\Handler\HandlersLocator;
17+
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
18+
19+
class HandlersLocatorTest extends TestCase
20+
{
21+
public function testItYieldsProvidedAliasAsKey()
22+
{
23+
$handler = $this->createPartialMock(\stdClass::class, array('__invoke'));
24+
$locator = new HandlersLocator(array(
25+
DummyMessage::class => array('dummy' => $handler),
26+
));
27+
28+
$this->assertSame(array('dummy' => $handler), iterator_to_array($locator->getHandlers(new Envelope(new DummyMessage('a')))));
29+
}
30+
}

src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Messenger\Handler\HandlersLocator;
1616
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
1717
use Symfony\Component\Messenger\Middleware\StackMiddleware;
18+
use Symfony\Component\Messenger\Stamp\HandledStamp;
1819
use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase;
1920
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
2021

@@ -36,6 +37,55 @@ public function testItCallsTheHandlerAndNextMiddleware()
3637
$middleware->handle($envelope, $this->getStackMock());
3738
}
3839

40+
/**
41+
* @dataProvider itAddsHandledStampsProvider
42+
*/
43+
public function testItAddsHandledStamps(array $handlers, array $expectedStamps)
44+
{
45+
$message = new DummyMessage('Hey');
46+
$envelope = new Envelope($message);
47+
48+
$middleware = new HandleMessageMiddleware(new HandlersLocator(array(
49+
DummyMessage::class => $handlers,
50+
)));
51+
52+
$envelope = $middleware->handle($envelope, $this->getStackMock());
53+
54+
$this->assertEquals($expectedStamps, $envelope->all(HandledStamp::class));
55+
}
56+
57+
public function itAddsHandledStampsProvider()
58+
{
59+
$first = $this->createPartialMock(\stdClass::class, array('__invoke'));
60+
$first->method('__invoke')->willReturn('first result');
61+
$firstClass = \get_class($first);
62+
63+
$second = $this->createPartialMock(\stdClass::class, array('__invoke'));
64+
$second->method('__invoke')->willReturn(null);
65+
$secondClass = \get_class($second);
66+
67+
yield 'A stamp is added' => array(
68+
array($first),
69+
array(new HandledStamp('first result', $firstClass.'::__invoke')),
70+
);
71+
72+
yield 'A stamp is added per handler' => array(
73+
array($first, $second),
74+
array(
75+
new HandledStamp('first result', $firstClass.'::__invoke'),
76+
new HandledStamp(null, $secondClass.'::__invoke'),
77+
),
78+
);
79+
80+
yield 'Yielded locator alias is used' => array(
81+
array('first_alias' => $first, $second),
82+
array(
83+
new HandledStamp('first result', $firstClass.'::__invoke', 'first_alias'),
84+
new HandledStamp(null, $secondClass.'::__invoke'),
85+
),
86+
);
87+
}
88+
3989
/**
4090
* @expectedException \Symfony\Component\Messenger\Exception\NoHandlerForMessageException
4191
* @expectedExceptionMessage No handler for message "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage"

src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Messenger\Envelope;
1515
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
1616
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
17+
use Symfony\Component\Messenger\Stamp\SentStamp;
1718
use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase;
1819
use Symfony\Component\Messenger\Tests\Fixtures\ChildDummyMessage;
1920
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
@@ -33,7 +34,12 @@ public function testItSendsTheMessageToAssignedSender()
3334

3435
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
3536

36-
$middleware->handle($envelope, $this->getStackMock(false));
37+
$envelope = $middleware->handle($envelope, $this->getStackMock(false));
38+
39+
/* @var SentStamp $stamp */
40+
$this->assertInstanceOf(SentStamp::class, $stamp = $envelope->last(SentStamp::class), 'it adds a sent stamp');
41+
$this->assertNull($stamp->getSenderAlias());
42+
$this->assertStringMatchesFormat('Mock_SenderInterface_%s', $stamp->getSenderClass());
3743
}
3844

3945
public function testItSendsTheMessageToAssignedSenderWithPreWrappedMessage()
@@ -128,6 +134,8 @@ public function testItSkipsReceivedMessages()
128134

129135
$sender->expects($this->never())->method('send');
130136

131-
$middleware->handle($envelope, $this->getStackMock());
137+
$envelope = $middleware->handle($envelope, $this->getStackMock());
138+
139+
$this->assertNull($envelope->last(SentStamp::class), 'it does not add sent stamp for received messages');
132140
}
133141
}

0 commit comments

Comments
 (0)
0