Skip to content

Commit

Permalink
feature #29166 [Messenger] Add handled & sent stamps (ogizanagi)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nicolas-grekas committed Nov 15, 2018
2 parents 100f205 + 2f5acf7 commit 88891d5
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 16 deletions.
Expand Up @@ -1578,9 +1578,10 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
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)
Expand Down
Expand Up @@ -569,7 +569,10 @@ public function testMessengerRouting()
);

$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());
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Messenger/Handler/HandlersLocator.php
Expand Up @@ -40,9 +40,9 @@ public function getHandlers(Envelope $envelope): iterable
$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;
}
}
}
Expand Down
Expand Up @@ -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;
}
Expand Up @@ -14,6 +14,7 @@
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>
Expand All @@ -40,8 +41,8 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$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())));
Expand Down
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\SentStamp;
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;

/**
Expand Down Expand Up @@ -42,8 +43,8 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
$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) {
Expand Down
89 changes: 89 additions & 0 deletions src/Symfony/Component/Messenger/Stamp/HandledStamp.php
@@ -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 changes: 43 additions & 0 deletions src/Symfony/Component/Messenger/Stamp/SentStamp.php
@@ -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')))));
}
}
Expand Up @@ -15,6 +15,7 @@
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;

Expand All @@ -36,6 +37,55 @@ public function testItCallsTheHandlerAndNextMiddleware()
$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"
Expand Down
Expand Up @@ -14,6 +14,7 @@
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;
Expand All @@ -33,7 +34,12 @@ public function testItSendsTheMessageToAssignedSender()

$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()
Expand Down Expand Up @@ -128,6 +134,8 @@ public function testItSkipsReceivedMessages()

$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 comments on commit 88891d5

Please sign in to comment.