--- /dev/null
+<?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;
+
+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 <maxime.steinhausser@gmail.com>
+ *
+ * @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();
+ }
+}
--- /dev/null
+<?php
+
+namespace Symfony\Component\Messenger\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\HandleTrait;
+use Symfony\Component\Messenger\MessageBus;
+use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Messenger\Stamp\HandledStamp;
+use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
+
+class HandleTraitTest extends TestCase
+{
+ /**
+ * @expectedException \Symfony\Component\Messenger\Exception\LogicException
+ * @expectedExceptionMessage You must provide a "Symfony\Component\Messenger\MessageBusInterface" instance in the "Symfony\Component\Messenger\Tests\TestQueryBus::$messageBus" property, "NULL" given.
+ */
+ public function testItThrowsOnNoMessageBusInstance()
+ {
+ $queryBus = new TestQueryBus(null);
+ $query = new DummyMessage('Hello');
+
+ $queryBus->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);
+ }
+}