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](1c1818b876/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 -------2f5acf790a
[Messenger] Add handled & sent stamps
This commit is contained in:
commit
88891d5e55
@ -1578,9 +1578,10 @@ class FrameworkExtension extends Extension
|
||||
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
|
||||
throw new LogicException(sprintf('Invalid Messenger routing configuration: class or interface "%s" not found.', $message));
|
||||
}
|
||||
$senders = array_map(function ($sender) use ($senderAliases) {
|
||||
return new Reference($senderAliases[$sender] ?? $sender);
|
||||
}, $messageConfiguration['senders']);
|
||||
$senders = array();
|
||||
foreach ($messageConfiguration['senders'] as $sender) {
|
||||
$senders[$sender] = new Reference($senderAliases[$sender] ?? $sender);
|
||||
}
|
||||
|
||||
$sendersId = 'messenger.senders.'.$message;
|
||||
$container->register($sendersId, RewindableGenerator::class)
|
||||
|
@ -569,7 +569,10 @@ abstract class FrameworkExtensionTest extends TestCase
|
||||
);
|
||||
|
||||
$this->assertSame($messageToSendAndHandleMapping, $senderLocatorDefinition->getArgument(1));
|
||||
$this->assertEquals(array(new Reference('messenger.transport.amqp'), new Reference('audit')), $container->getDefinition('messenger.senders.'.DummyMessage::class)->getArgument(0)[0]->getValues());
|
||||
$this->assertEquals(array(
|
||||
'amqp' => new Reference('messenger.transport.amqp'),
|
||||
'audit' => new Reference('audit'),
|
||||
), $container->getDefinition('messenger.senders.'.DummyMessage::class)->getArgument(0)[0]->getValues());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,6 +4,7 @@ CHANGELOG
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* Added `HandledStamp` & `SentStamp` stamps
|
||||
* All the changes below are BC BREAKS
|
||||
* Senders and handlers subscribing to parent interfaces now receive *all* matching messages, wildcard included
|
||||
* `MessageBusInterface::dispatch()`, `MiddlewareInterface::handle()` and `SenderInterface::send()` return `Envelope`
|
||||
|
@ -40,9 +40,9 @@ class HandlersLocator implements HandlersLocatorInterface
|
||||
$seen = array();
|
||||
|
||||
foreach (self::listTypes($envelope) as $type) {
|
||||
foreach ($this->handlers[$type] ?? array() as $handler) {
|
||||
foreach ($this->handlers[$type] ?? array() as $alias => $handler) {
|
||||
if (!\in_array($handler, $seen, true)) {
|
||||
yield $seen[] = $handler;
|
||||
yield $alias => $seen[] = $handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ interface HandlersLocatorInterface
|
||||
/**
|
||||
* Returns the handlers for the given message name.
|
||||
*
|
||||
* @return iterable|callable[]
|
||||
* @return iterable|callable[] Indexed by handler alias if available
|
||||
*/
|
||||
public function getHandlers(Envelope $envelope): iterable;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Messenger\Middleware;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\NoHandlerForMessageException;
|
||||
use Symfony\Component\Messenger\Handler\HandlersLocatorInterface;
|
||||
use Symfony\Component\Messenger\Stamp\HandledStamp;
|
||||
|
||||
/**
|
||||
* @author Samuel Roze <samuel.roze@gmail.com>
|
||||
@ -40,8 +41,8 @@ class HandleMessageMiddleware implements MiddlewareInterface
|
||||
{
|
||||
$handler = null;
|
||||
$message = $envelope->getMessage();
|
||||
foreach ($this->handlersLocator->getHandlers($envelope) as $handler) {
|
||||
$handler($message);
|
||||
foreach ($this->handlersLocator->getHandlers($envelope) as $alias => $handler) {
|
||||
$envelope = $envelope->with(HandledStamp::fromCallable($handler, $handler($message), \is_string($alias) ? $alias : null));
|
||||
}
|
||||
if (null === $handler && !$this->allowNoHandlers) {
|
||||
throw new NoHandlerForMessageException(sprintf('No handler for message "%s".', \get_class($envelope->getMessage())));
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Messenger\Middleware;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
|
||||
use Symfony\Component\Messenger\Stamp\SentStamp;
|
||||
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
|
||||
|
||||
/**
|
||||
@ -42,8 +43,8 @@ class SendMessageMiddleware implements MiddlewareInterface
|
||||
$handle = false;
|
||||
$sender = null;
|
||||
|
||||
foreach ($this->sendersLocator->getSenders($envelope, $handle) as $sender) {
|
||||
$envelope = $sender->send($envelope);
|
||||
foreach ($this->sendersLocator->getSenders($envelope, $handle) as $alias => $sender) {
|
||||
$envelope = $sender->send($envelope)->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null));
|
||||
}
|
||||
|
||||
if (null === $sender || $handle) {
|
||||
|
89
src/Symfony/Component/Messenger/Stamp/HandledStamp.php
Normal file
89
src/Symfony/Component/Messenger/Stamp/HandledStamp.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?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\Stamp;
|
||||
|
||||
/**
|
||||
* Stamp identifying a message handled by the `HandleMessageMiddleware` middleware
|
||||
* and storing the handler returned value.
|
||||
*
|
||||
* @see \Symfony\Component\Messenger\Middleware\HandleMessageMiddleware
|
||||
*
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*
|
||||
* @experimental in 4.2
|
||||
*/
|
||||
final class HandledStamp implements StampInterface
|
||||
{
|
||||
private $result;
|
||||
private $callableName;
|
||||
private $handlerAlias;
|
||||
|
||||
/**
|
||||
* @param mixed $result The returned value of the message handler
|
||||
*/
|
||||
public function __construct($result, string $callableName, string $handlerAlias = null)
|
||||
{
|
||||
$this->result = $result;
|
||||
$this->callableName = $callableName;
|
||||
$this->handlerAlias = $handlerAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $result The returned value of the message handler
|
||||
*/
|
||||
public static function fromCallable(callable $handler, $result, string $handlerAlias = null): self
|
||||
{
|
||||
if (\is_array($handler)) {
|
||||
if (\is_object($handler[0])) {
|
||||
return new self($result, \get_class($handler[0]).'::'.$handler[1], $handlerAlias);
|
||||
}
|
||||
|
||||
return new self($result, $handler[0].'::'.$handler[1], $handlerAlias);
|
||||
}
|
||||
|
||||
if (\is_string($handler)) {
|
||||
return new self($result, $handler, $handlerAlias);
|
||||
}
|
||||
|
||||
if ($handler instanceof \Closure) {
|
||||
$r = new \ReflectionFunction($handler);
|
||||
if (false !== strpos($r->name, '{closure}')) {
|
||||
return new self($result, 'Closure', $handlerAlias);
|
||||
}
|
||||
if ($class = $r->getClosureScopeClass()) {
|
||||
return new self($result, $class->name.'::'.$r->name, $handlerAlias);
|
||||
}
|
||||
|
||||
return new self($result, $r->name, $handlerAlias);
|
||||
}
|
||||
|
||||
return new self($result, \get_class($handler).'::__invoke', $handlerAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function getCallableName(): string
|
||||
{
|
||||
return $this->callableName;
|
||||
}
|
||||
|
||||
public function getHandlerAlias(): ?string
|
||||
{
|
||||
return $this->handlerAlias;
|
||||
}
|
||||
}
|
43
src/Symfony/Component/Messenger/Stamp/SentStamp.php
Normal file
43
src/Symfony/Component/Messenger/Stamp/SentStamp.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?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\Stamp;
|
||||
|
||||
/**
|
||||
* Marker stamp identifying a message sent by the `SendMessageMiddleware`.
|
||||
*
|
||||
* @see \Symfony\Component\Messenger\Middleware\SendMessageMiddleware
|
||||
*
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*
|
||||
* @experimental in 4.2
|
||||
*/
|
||||
final class SentStamp implements StampInterface
|
||||
{
|
||||
private $senderClass;
|
||||
private $senderAlias;
|
||||
|
||||
public function __construct(string $senderClass, string $senderAlias = null)
|
||||
{
|
||||
$this->senderAlias = $senderAlias;
|
||||
$this->senderClass = $senderClass;
|
||||
}
|
||||
|
||||
public function getSenderClass(): string
|
||||
{
|
||||
return $this->senderClass;
|
||||
}
|
||||
|
||||
public function getSenderAlias(): ?string
|
||||
{
|
||||
return $this->senderAlias;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?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\Tests\Handler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Handler\HandlersLocator;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
|
||||
class HandlersLocatorTest extends TestCase
|
||||
{
|
||||
public function testItYieldsProvidedAliasAsKey()
|
||||
{
|
||||
$handler = $this->createPartialMock(\stdClass::class, array('__invoke'));
|
||||
$locator = new HandlersLocator(array(
|
||||
DummyMessage::class => array('dummy' => $handler),
|
||||
));
|
||||
|
||||
$this->assertSame(array('dummy' => $handler), iterator_to_array($locator->getHandlers(new Envelope(new DummyMessage('a')))));
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Handler\HandlersLocator;
|
||||
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
|
||||
use Symfony\Component\Messenger\Middleware\StackMiddleware;
|
||||
use Symfony\Component\Messenger\Stamp\HandledStamp;
|
||||
use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
|
||||
@ -36,6 +37,55 @@ class HandleMessageMiddlewareTest extends MiddlewareTestCase
|
||||
$middleware->handle($envelope, $this->getStackMock());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider itAddsHandledStampsProvider
|
||||
*/
|
||||
public function testItAddsHandledStamps(array $handlers, array $expectedStamps)
|
||||
{
|
||||
$message = new DummyMessage('Hey');
|
||||
$envelope = new Envelope($message);
|
||||
|
||||
$middleware = new HandleMessageMiddleware(new HandlersLocator(array(
|
||||
DummyMessage::class => $handlers,
|
||||
)));
|
||||
|
||||
$envelope = $middleware->handle($envelope, $this->getStackMock());
|
||||
|
||||
$this->assertEquals($expectedStamps, $envelope->all(HandledStamp::class));
|
||||
}
|
||||
|
||||
public function itAddsHandledStampsProvider()
|
||||
{
|
||||
$first = $this->createPartialMock(\stdClass::class, array('__invoke'));
|
||||
$first->method('__invoke')->willReturn('first result');
|
||||
$firstClass = \get_class($first);
|
||||
|
||||
$second = $this->createPartialMock(\stdClass::class, array('__invoke'));
|
||||
$second->method('__invoke')->willReturn(null);
|
||||
$secondClass = \get_class($second);
|
||||
|
||||
yield 'A stamp is added' => array(
|
||||
array($first),
|
||||
array(new HandledStamp('first result', $firstClass.'::__invoke')),
|
||||
);
|
||||
|
||||
yield 'A stamp is added per handler' => array(
|
||||
array($first, $second),
|
||||
array(
|
||||
new HandledStamp('first result', $firstClass.'::__invoke'),
|
||||
new HandledStamp(null, $secondClass.'::__invoke'),
|
||||
),
|
||||
);
|
||||
|
||||
yield 'Yielded locator alias is used' => array(
|
||||
array('first_alias' => $first, $second),
|
||||
array(
|
||||
new HandledStamp('first result', $firstClass.'::__invoke', 'first_alias'),
|
||||
new HandledStamp(null, $secondClass.'::__invoke'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Messenger\Exception\NoHandlerForMessageException
|
||||
* @expectedExceptionMessage No handler for message "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage"
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Messenger\Tests\Middleware;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
|
||||
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
|
||||
use Symfony\Component\Messenger\Stamp\SentStamp;
|
||||
use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\ChildDummyMessage;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
|
||||
@ -33,7 +34,12 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
|
||||
|
||||
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
|
||||
|
||||
$middleware->handle($envelope, $this->getStackMock(false));
|
||||
$envelope = $middleware->handle($envelope, $this->getStackMock(false));
|
||||
|
||||
/* @var SentStamp $stamp */
|
||||
$this->assertInstanceOf(SentStamp::class, $stamp = $envelope->last(SentStamp::class), 'it adds a sent stamp');
|
||||
$this->assertNull($stamp->getSenderAlias());
|
||||
$this->assertStringMatchesFormat('Mock_SenderInterface_%s', $stamp->getSenderClass());
|
||||
}
|
||||
|
||||
public function testItSendsTheMessageToAssignedSenderWithPreWrappedMessage()
|
||||
@ -128,6 +134,8 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
|
||||
|
||||
$sender->expects($this->never())->method('send');
|
||||
|
||||
$middleware->handle($envelope, $this->getStackMock());
|
||||
$envelope = $middleware->handle($envelope, $this->getStackMock());
|
||||
|
||||
$this->assertNull($envelope->last(SentStamp::class), 'it does not add sent stamp for received messages');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
<?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\Tests\Stamp;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Stamp\HandledStamp;
|
||||
use Symfony\Component\Messenger\Tests\Fixtures\DummyCommandHandler;
|
||||
|
||||
class HandledStampTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$stamp = new HandledStamp('some result', 'FooHandler::__invoke()', 'foo');
|
||||
|
||||
$this->assertSame('some result', $stamp->getResult());
|
||||
$this->assertSame('FooHandler::__invoke()', $stamp->getCallableName());
|
||||
$this->assertSame('foo', $stamp->getHandlerAlias());
|
||||
|
||||
$stamp = new HandledStamp('some result', 'FooHandler::__invoke()');
|
||||
|
||||
$this->assertSame('some result', $stamp->getResult());
|
||||
$this->assertSame('FooHandler::__invoke()', $stamp->getCallableName());
|
||||
$this->assertNull($stamp->getHandlerAlias());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideCallables
|
||||
*/
|
||||
public function testFromCallable(callable $handler, ?string $expectedHandlerString)
|
||||
{
|
||||
/** @var HandledStamp $stamp */
|
||||
$stamp = HandledStamp::fromCallable($handler, 'some_result', 'alias');
|
||||
$this->assertStringMatchesFormat($expectedHandlerString, $stamp->getCallableName());
|
||||
$this->assertSame('alias', $stamp->getHandlerAlias(), 'alias is forwarded to construct');
|
||||
$this->assertSame('some_result', $stamp->getResult(), 'result is forwarded to construct');
|
||||
}
|
||||
|
||||
public function provideCallables()
|
||||
{
|
||||
yield array(function () {}, 'Closure');
|
||||
yield array('var_dump', 'var_dump');
|
||||
yield array(new DummyCommandHandler(), DummyCommandHandler::class.'::__invoke');
|
||||
yield array(
|
||||
array(new DummyCommandHandlerWithSpecificMethod(), 'handle'),
|
||||
DummyCommandHandlerWithSpecificMethod::class.'::handle',
|
||||
);
|
||||
yield array(\Closure::fromCallable(function () {}), 'Closure');
|
||||
yield array(\Closure::fromCallable(new DummyCommandHandler()), DummyCommandHandler::class.'::__invoke');
|
||||
yield array(\Closure::bind(\Closure::fromCallable(function () {}), new \stdClass()), 'Closure');
|
||||
yield array(new class() {
|
||||
public function __invoke()
|
||||
{
|
||||
}
|
||||
}, 'class@anonymous%sHandledStampTest.php%s::__invoke');
|
||||
}
|
||||
}
|
||||
|
||||
class DummyCommandHandlerWithSpecificMethod
|
||||
{
|
||||
public function handle(): void
|
||||
{
|
||||
}
|
||||
}
|
@ -30,4 +30,14 @@ class SendersLocatorTest extends TestCase
|
||||
$this->assertSame(array($sender), iterator_to_array($locator->getSenders(new Envelope(new DummyMessage('a')))));
|
||||
$this->assertSame(array(), iterator_to_array($locator->getSenders(new Envelope(new SecondMessage()))));
|
||||
}
|
||||
|
||||
public function testItYieldsProvidedSenderAliasAsKey()
|
||||
{
|
||||
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
|
||||
$locator = new SendersLocator(array(
|
||||
DummyMessage::class => array('dummy' => $sender),
|
||||
));
|
||||
|
||||
$this->assertSame(array('dummy' => $sender), iterator_to_array($locator->getSenders(new Envelope(new DummyMessage('a')))));
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,9 @@ class SendersLocator implements SendersLocatorInterface
|
||||
$seen = array();
|
||||
|
||||
foreach (HandlersLocator::listTypes($envelope) as $type) {
|
||||
foreach ($this->senders[$type] ?? array() as $sender) {
|
||||
foreach ($this->senders[$type] ?? array() as $alias => $sender) {
|
||||
if (!\in_array($sender, $seen, true)) {
|
||||
yield $seen[] = $sender;
|
||||
yield $alias => $seen[] = $sender;
|
||||
}
|
||||
}
|
||||
$handle = $handle ?: $this->sendAndHandle[$type] ?? false;
|
||||
|
@ -29,7 +29,7 @@ interface SendersLocatorInterface
|
||||
* @param bool|null &$handle True after calling the method when the next middleware
|
||||
* should also get the message; false otherwise
|
||||
*
|
||||
* @return iterable|SenderInterface[]
|
||||
* @return iterable|SenderInterface[] Indexed by sender alias if available
|
||||
*/
|
||||
public function getSenders(Envelope $envelope, ?bool &$handle = false): iterable;
|
||||
}
|
||||
|
Reference in New Issue
Block a user