From 6ba4e8aad5f5f2e3779538181ae3c3497e17acfe Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Sat, 10 Nov 2018 17:00:31 +0100 Subject: [PATCH] [Messenger] Add a trait for synchronous query & command buses --- src/Symfony/Component/Messenger/CHANGELOG.md | 2 + .../Component/Messenger/HandleTrait.php | 63 ++++++++++++ .../Messenger/Tests/HandleTraitTest.php | 97 +++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 src/Symfony/Component/Messenger/HandleTrait.php create mode 100644 src/Symfony/Component/Messenger/Tests/HandleTraitTest.php diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 7034c9ec8f..eb1268e25e 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 4.2.0 ----- + * Added `HandleTrait` leveraging a message bus instance to return a single + synchronous message handling result * 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 diff --git a/src/Symfony/Component/Messenger/HandleTrait.php b/src/Symfony/Component/Messenger/HandleTrait.php new file mode 100644 index 0000000000..9224d13fd3 --- /dev/null +++ b/src/Symfony/Component/Messenger/HandleTrait.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Stamp\HandledStamp; + +/** + * Leverages a message bus to expect a single, synchronous message handling and return its result. + * + * @author Maxime Steinhausser + * + * @experimental in 4.2 + */ +trait HandleTrait +{ + /** @var MessageBusInterface */ + private $messageBus; + + /** + * Dispatches the given message, expecting to be handled by a single handler + * and returns the result from the handler returned value. + * This behavior is useful for both synchronous command & query buses, + * the last one usually returning the handler result. + * + * @param object|Envelope $message The message or the message pre-wrapped in an envelope + * + * @return mixed The handler returned value + */ + private function handle($message) + { + if (!$this->messageBus instanceof MessageBusInterface) { + throw new LogicException(sprintf('You must provide a "%s" instance in the "%s::$messageBus" property, "%s" given.', MessageBusInterface::class, \get_class($this), \is_object($this->messageBus) ? \get_class($this->messageBus) : \gettype($this->messageBus))); + } + + $envelope = $this->messageBus->dispatch($message); + /** @var HandledStamp[] $handledStamps */ + $handledStamps = $envelope->all(HandledStamp::class); + + if (!$handledStamps) { + throw new LogicException(sprintf('Message of type "%s" was handled zero times. Exactly one handler is expected when using "%s::%s()".', \get_class($envelope->getMessage()), \get_class($this), __FUNCTION__)); + } + + if (\count($handledStamps) > 1) { + $handlers = implode(', ', array_map(function (HandledStamp $stamp): string { + return sprintf('"%s"', $stamp->getHandlerAlias() ?? $stamp->getCallableName()); + }, $handledStamps)); + + throw new LogicException(sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: %s.', \get_class($envelope->getMessage()), \get_class($this), __FUNCTION__, \count($handledStamps), $handlers)); + } + + return $handledStamps[0]->getResult(); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php b/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php new file mode 100644 index 0000000000..a4dbc2de99 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php @@ -0,0 +1,97 @@ +query($query); + } + + public function testHandleReturnsHandledStampResult() + { + $bus = $this->createMock(MessageBus::class); + $queryBus = new TestQueryBus($bus); + + $query = new DummyMessage('Hello'); + $bus->expects($this->once())->method('dispatch')->willReturn( + new Envelope($query, new HandledStamp('result', 'DummyHandler::__invoke')) + ); + + $this->assertSame('result', $queryBus->query($query)); + } + + public function testHandleAcceptsEnvelopes() + { + $bus = $this->createMock(MessageBus::class); + $queryBus = new TestQueryBus($bus); + + $envelope = new Envelope(new DummyMessage('Hello'), new HandledStamp('result', 'DummyHandler::__invoke')); + $bus->expects($this->once())->method('dispatch')->willReturn($envelope); + + $this->assertSame('result', $queryBus->query($envelope)); + } + + /** + * @expectedException \Symfony\Component\Messenger\Exception\LogicException + * @expectedExceptionMessage Message of type "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" was handled zero times. Exactly one handler is expected when using "Symfony\Component\Messenger\Tests\TestQueryBus::handle()". + */ + public function testHandleThrowsOnNoHandledStamp() + { + $bus = $this->createMock(MessageBus::class); + $queryBus = new TestQueryBus($bus); + + $query = new DummyMessage('Hello'); + $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope($query)); + + $queryBus->query($query); + } + + /** + * @expectedException \Symfony\Component\Messenger\Exception\LogicException + * @expectedExceptionMessage Message of type "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" was handled multiple times. Only one handler is expected when using "Symfony\Component\Messenger\Tests\TestQueryBus::handle()", got 2: "FirstDummyHandler::__invoke", "dummy_2". + */ + public function testHandleThrowsOnMultipleHandledStamps() + { + $bus = $this->createMock(MessageBus::class); + $queryBus = new TestQueryBus($bus); + + $query = new DummyMessage('Hello'); + $bus->expects($this->once())->method('dispatch')->willReturn( + new Envelope($query, new HandledStamp('first_result', 'FirstDummyHandler::__invoke'), new HandledStamp('second_result', 'SecondDummyHandler::__invoke', 'dummy_2')) + ); + + $queryBus->query($query); + } +} + +class TestQueryBus +{ + use HandleTrait; + + public function __construct(?MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + public function query($query): string + { + return $this->handle($query); + } +}