diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9487f7e265ad..ff01fcc052df 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index f525b5f6a78f..0e16447cf582 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -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()); } /** diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index d1e85da795d8..7034c9ec8fcc 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -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` diff --git a/src/Symfony/Component/Messenger/Handler/HandlersLocator.php b/src/Symfony/Component/Messenger/Handler/HandlersLocator.php index 8d256fe74b65..d0642498bab8 100644 --- a/src/Symfony/Component/Messenger/Handler/HandlersLocator.php +++ b/src/Symfony/Component/Messenger/Handler/HandlersLocator.php @@ -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; } } } diff --git a/src/Symfony/Component/Messenger/Handler/HandlersLocatorInterface.php b/src/Symfony/Component/Messenger/Handler/HandlersLocatorInterface.php index 4a4342c0007b..48673569a645 100644 --- a/src/Symfony/Component/Messenger/Handler/HandlersLocatorInterface.php +++ b/src/Symfony/Component/Messenger/Handler/HandlersLocatorInterface.php @@ -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; } diff --git a/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php b/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php index ecc0eb680b22..ba89051f24d3 100644 --- a/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php +++ b/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php @@ -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 @@ -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()))); diff --git a/src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php b/src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php index 435e146a3a34..7c0ee4a48887 100644 --- a/src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php +++ b/src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php @@ -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; /** @@ -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) { diff --git a/src/Symfony/Component/Messenger/Stamp/HandledStamp.php b/src/Symfony/Component/Messenger/Stamp/HandledStamp.php new file mode 100644 index 000000000000..0cd480765ecb --- /dev/null +++ b/src/Symfony/Component/Messenger/Stamp/HandledStamp.php @@ -0,0 +1,89 @@ + + * + * 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 + * + * @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; + } +} diff --git a/src/Symfony/Component/Messenger/Stamp/SentStamp.php b/src/Symfony/Component/Messenger/Stamp/SentStamp.php new file mode 100644 index 000000000000..b0b8da8b1670 --- /dev/null +++ b/src/Symfony/Component/Messenger/Stamp/SentStamp.php @@ -0,0 +1,43 @@ + + * + * 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 + * + * @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; + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php b/src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php new file mode 100644 index 000000000000..4b4e84246d80 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php @@ -0,0 +1,30 @@ + + * + * 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'))))); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php index 92512c9b8ce9..457428c435be 100644 --- a/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php +++ b/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php @@ -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; @@ -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" diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php index 50c14617e87f..572f237b5d2a 100644 --- a/src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php +++ b/src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php @@ -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; @@ -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() @@ -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'); } } diff --git a/src/Symfony/Component/Messenger/Tests/Stamp/HandledStampTest.php b/src/Symfony/Component/Messenger/Tests/Stamp/HandledStampTest.php new file mode 100644 index 000000000000..72584c67404b --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Stamp/HandledStampTest.php @@ -0,0 +1,72 @@ + + * + * 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 + { + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php index f756121e094b..e693cee53a70 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php @@ -30,4 +30,14 @@ public function testItReturnsTheSenderBasedOnTheMessageClass() $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'))))); + } } diff --git a/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php b/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php index d0e1c8ac29f0..47f64a6d14ff 100644 --- a/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php +++ b/src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php @@ -46,9 +46,9 @@ public function getSenders(Envelope $envelope, ?bool &$handle = false): iterable $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; diff --git a/src/Symfony/Component/Messenger/Transport/Sender/SendersLocatorInterface.php b/src/Symfony/Component/Messenger/Transport/Sender/SendersLocatorInterface.php index ff367926460c..e802bebff4de 100644 --- a/src/Symfony/Component/Messenger/Transport/Sender/SendersLocatorInterface.php +++ b/src/Symfony/Component/Messenger/Transport/Sender/SendersLocatorInterface.php @@ -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; }