[Messenger] Add a trait for synchronous query & command buses
authorMaxime Steinhausser <maxime.steinhausser@gmail.com>
Sat, 10 Nov 2018 16:00:31 +0000 (17:00 +0100)
committerMaxime Steinhausser <maxime.steinhausser@gmail.com>
Tue, 20 Nov 2018 18:19:09 +0000 (19:19 +0100)
src/Symfony/Component/Messenger/CHANGELOG.md
src/Symfony/Component/Messenger/HandleTrait.php [new file with mode: 0644]
src/Symfony/Component/Messenger/Tests/HandleTraitTest.php [new file with mode: 0644]

index 7034c9e..eb1268e 100644 (file)
@@ -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 (file)
index 0000000..9224d13
--- /dev/null
@@ -0,0 +1,63 @@
+<?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();
+    }
+}
diff --git a/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php b/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php
new file mode 100644 (file)
index 0000000..a4dbc2d
--- /dev/null
@@ -0,0 +1,97 @@
+<?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);
+    }
+}