[Messenger] Add handled & sent stamps
authorMaxime Steinhausser <maxime.steinhausser@gmail.com>
Sat, 10 Nov 2018 15:53:27 +0000 (16:53 +0100)
committerMaxime Steinhausser <maxime.steinhausser@elao.com>
Thu, 15 Nov 2018 09:18:06 +0000 (10:18 +0100)
16 files changed:
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
src/Symfony/Component/Messenger/CHANGELOG.md
src/Symfony/Component/Messenger/Handler/HandlersLocator.php
src/Symfony/Component/Messenger/Handler/HandlersLocatorInterface.php
src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php
src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php
src/Symfony/Component/Messenger/Stamp/HandledStamp.php [new file with mode: 0644]
src/Symfony/Component/Messenger/Stamp/SentStamp.php [new file with mode: 0644]
src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php [new file with mode: 0644]
src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php
src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php
src/Symfony/Component/Messenger/Tests/Stamp/HandledStampTest.php [new file with mode: 0644]
src/Symfony/Component/Messenger/Tests/Transport/Sender/SendersLocatorTest.php
src/Symfony/Component/Messenger/Transport/Sender/SendersLocator.php
src/Symfony/Component/Messenger/Transport/Sender/SendersLocatorInterface.php

index 9487f7e..ff01fcc 100644 (file)
@@ -1578,9 +1578,10 @@ class FrameworkExtension extends Extension
             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)
index f525b5f..0e16447 100644 (file)
@@ -569,7 +569,10 @@ abstract class FrameworkExtensionTest extends TestCase
         );
 
         $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());
     }
 
     /**
index d1e85da..7034c9e 100644 (file)
@@ -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`
index 8d256fe..d064249 100644 (file)
@@ -40,9 +40,9 @@ class HandlersLocator implements HandlersLocatorInterface
         $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;
                 }
             }
         }
index 4a4342c..4867356 100644 (file)
@@ -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;
 }
index ecc0eb6..ba89051 100644 (file)
@@ -14,6 +14,7 @@ namespace Symfony\Component\Messenger\Middleware;
 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>
@@ -40,8 +41,8 @@ class HandleMessageMiddleware implements MiddlewareInterface
     {
         $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())));
index 435e146..7c0ee4a 100644 (file)
@@ -13,6 +13,7 @@ namespace Symfony\Component\Messenger\Middleware;
 
 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 @@ class SendMessageMiddleware implements MiddlewareInterface
         $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 (file)
index 0000000..0cd4807
--- /dev/null
@@ -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;
+    }
+}
diff --git a/src/Symfony/Component/Messenger/Stamp/SentStamp.php b/src/Symfony/Component/Messenger/Stamp/SentStamp.php
new file mode 100644 (file)
index 0000000..b0b8da8
--- /dev/null
@@ -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;
+    }
+}
diff --git a/src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php b/src/Symfony/Component/Messenger/Tests/Handler/HandlersLocatorTest.php
new file mode 100644 (file)
index 0000000..4b4e842
--- /dev/null
@@ -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')))));
+    }
+}
index 92512c9..457428c 100644 (file)
@@ -15,6 +15,7 @@ use Symfony\Component\Messenger\Envelope;
 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 @@ class HandleMessageMiddlewareTest extends MiddlewareTestCase
         $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"
index 50c1461..572f237 100644 (file)
@@ -14,6 +14,7 @@ namespace Symfony\Component\Messenger\Tests\Middleware;
 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 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
 
         $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 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
 
         $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 (file)
index 0000000..72584c6
--- /dev/null
@@ -0,0 +1,72 @@
+<?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\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
+    {
+    }
+}
index f756121..e693cee 100644 (file)
@@ -30,4 +30,14 @@ class SendersLocatorTest extends TestCase
         $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')))));
+    }
 }
index d0e1c8a..47f64a6 100644 (file)
@@ -46,9 +46,9 @@ class SendersLocator implements SendersLocatorInterface
         $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;
index ff36792..e802beb 100644 (file)
@@ -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;
 }