From 3cc685738279dbdc8ac332fd1f73c2dcd755d393 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 4 Jul 2018 15:38:36 +0200 Subject: [PATCH] Adding support for record messages --- .../Resources/config/messenger.xml | 11 ++ .../Exception/MessageHandlingException.php | 41 +++++ .../Component/Messenger/MessageRecorder.php | 55 ++++++ .../Messenger/MessageRecorderInterface.php | 26 +++ .../HandleRecordedMessageMiddleware.php | 71 ++++++++ .../RecordedMessageCollectionInterface.php | 31 ++++ .../HandleRecordedMessageMiddlewareTest.php | 161 ++++++++++++++++++ src/Symfony/Component/Messenger/composer.json | 1 + 8 files changed, 397 insertions(+) create mode 100644 src/Symfony/Component/Messenger/Exception/MessageHandlingException.php create mode 100644 src/Symfony/Component/Messenger/MessageRecorder.php create mode 100644 src/Symfony/Component/Messenger/MessageRecorderInterface.php create mode 100644 src/Symfony/Component/Messenger/Middleware/HandleRecordedMessageMiddleware.php create mode 100644 src/Symfony/Component/Messenger/RecordedMessageCollectionInterface.php create mode 100644 src/Symfony/Component/Messenger/Tests/Middleware/HandleRecordedMessageMiddlewareTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index 04823bce0ebeb..a06a6c5215bd0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -34,6 +34,11 @@ + + + + + @@ -62,5 +67,11 @@ + + + + + + diff --git a/src/Symfony/Component/Messenger/Exception/MessageHandlingException.php b/src/Symfony/Component/Messenger/Exception/MessageHandlingException.php new file mode 100644 index 0000000000000..63f7595d63d7b --- /dev/null +++ b/src/Symfony/Component/Messenger/Exception/MessageHandlingException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * When handling messages, some handlers caused an exception. This exception + * contains all those handler exceptions. + * + * @author Tobias Nyholm + */ +class MessageHandlingException extends \RuntimeException implements ExceptionInterface +{ + private $exceptions = array(); + + public function __construct(array $exceptions) + { + $message = sprintf( + "Some handlers for recorded messages threw an exception. Their messages were: \n\n%s", + implode(", \n", array_map(function (\Throwable $e) { + return $e->getMessage(); + }, $exceptions)) + ); + + $this->exceptions = $exceptions; + parent::__construct($message); + } + + public function getExceptions(): array + { + return $this->exceptions; + } +} diff --git a/src/Symfony/Component/Messenger/MessageRecorder.php b/src/Symfony/Component/Messenger/MessageRecorder.php new file mode 100644 index 0000000000000..f57037c16583c --- /dev/null +++ b/src/Symfony/Component/Messenger/MessageRecorder.php @@ -0,0 +1,55 @@ + + * + * 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\Contracts\Service\ResetInterface; + +/** + * @author Tobias Nyholm + * @author Matthias Noback + */ +class MessageRecorder implements MessageRecorderInterface, RecordedMessageCollectionInterface, ResetInterface +{ + private $messages = array(); + + /** + * {@inheritdoc} + */ + public function getRecordedMessages(): array + { + return $this->messages; + } + + /** + * {@inheritdoc} + */ + public function resetRecordedMessages(): void + { + $this->reset(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->messages = array(); + } + + /** + * {@inheritdoc} + */ + public function record($message): void + { + $this->messages[] = $message; + } +} diff --git a/src/Symfony/Component/Messenger/MessageRecorderInterface.php b/src/Symfony/Component/Messenger/MessageRecorderInterface.php new file mode 100644 index 0000000000000..4272e073eb118 --- /dev/null +++ b/src/Symfony/Component/Messenger/MessageRecorderInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * @author Tobias Nyholm + * @author Matthias Noback + */ +interface MessageRecorderInterface +{ + /** + * Record a message. + * + * @param object $message + */ + public function record($message); +} diff --git a/src/Symfony/Component/Messenger/Middleware/HandleRecordedMessageMiddleware.php b/src/Symfony/Component/Messenger/Middleware/HandleRecordedMessageMiddleware.php new file mode 100644 index 0000000000000..07d190e49e218 --- /dev/null +++ b/src/Symfony/Component/Messenger/Middleware/HandleRecordedMessageMiddleware.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware; + +use Symfony\Component\Messenger\Exception\MessageHandlingException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\RecordedMessageCollectionInterface; + +/** + * A middleware that takes all recorded messages and dispatch them to the bus. + * + * @author Tobias Nyholm + * @author Matthias Noback + */ +class HandleRecordedMessageMiddleware implements MiddlewareInterface +{ + private $messageRecorder; + private $messageBus; + + public function __construct(MessageBusInterface $messageBus, RecordedMessageCollectionInterface $messageRecorder) + { + $this->messageRecorder = $messageRecorder; + $this->messageBus = $messageBus; + } + + public function handle($message, callable $next) + { + // Make sure the recorder is empty before we begin + $this->messageRecorder->resetRecordedMessages(); + + try { + $returnData = $next($message); + } catch (\Throwable $exception) { + $this->messageRecorder->resetRecordedMessages(); + + throw $exception; + } + + $exceptions = array(); + while (!empty($recordedMessages = $this->messageRecorder->getRecordedMessages())) { + $this->messageRecorder->resetRecordedMessages(); + // Assert: The message recorder is empty, all messages are in $recordedMessages + + foreach ($recordedMessages as $recordedMessage) { + try { + $this->messageBus->dispatch($recordedMessage); + } catch (\Throwable $exception) { + $exceptions[] = $exception; + } + } + } + + if (!empty($exceptions)) { + if (1 === \count($exceptions)) { + throw $exceptions[0]; + } + throw new MessageHandlingException($exceptions); + } + + return $returnData; + } +} diff --git a/src/Symfony/Component/Messenger/RecordedMessageCollectionInterface.php b/src/Symfony/Component/Messenger/RecordedMessageCollectionInterface.php new file mode 100644 index 0000000000000..1bd6272ad68fb --- /dev/null +++ b/src/Symfony/Component/Messenger/RecordedMessageCollectionInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * @author Tobias Nyholm + * @author Matthias Noback + */ +interface RecordedMessageCollectionInterface +{ + /** + * Fetch recorded messages. + * + * @return object[] + */ + public function getRecordedMessages(): array; + + /** + * Remove all recorded messages. + */ + public function resetRecordedMessages(): void; +} diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/HandleRecordedMessageMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/HandleRecordedMessageMiddlewareTest.php new file mode 100644 index 0000000000000..9662543f952fa --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Middleware/HandleRecordedMessageMiddlewareTest.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Middleware; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Exception\MessageHandlingException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\MessageRecorder; +use Symfony\Component\Messenger\MessageRecorderInterface; +use Symfony\Component\Messenger\Middleware\HandleRecordedMessageMiddleware; +use Symfony\Component\Messenger\RecordedMessageCollectionInterface; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; + +class HandleRecordedMessageMiddlewareTest extends TestCase +{ + public function testResetRecorderOnException() + { + $message = new DummyMessage('Hello'); + $messageBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $recorder = new MessageRecorder(); + $next = new class($recorder) { + private $recorder; + + public function __construct(MessageRecorderInterface $recorder) + { + $this->recorder = $recorder; + } + + public function __invoke() + { + $this->recorder->record(new \stdClass()); + throw new \LogicException(); + } + }; + + $middleware = new HandleRecordedMessageMiddleware($messageBus, $recorder); + try { + $middleware->handle($message, $next); + } catch (\LogicException $e) { + } + + $this->assertEmpty($recorder->getRecordedMessages()); + } + + public function testResetRecorderOnStart() + { + $message = new DummyMessage('Hello'); + $recorder = new MessageRecorder(); + $recorder->record(new \stdClass()); + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next->expects($this->once())->method('__invoke')->willReturn('World'); + $messageBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $messageBus->expects($this->exactly(0))->method('dispatch'); + + $middleware = new HandleRecordedMessageMiddleware($messageBus, $recorder); + $middleware->handle($message, $next); + $this->assertEmpty($recorder->getRecordedMessages()); + } + + public function testDispatchMessageToBus() + { + $message = new DummyMessage('Hello'); + $recorder = new MessageRecorder(); + $messageBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $messageBus->expects($this->exactly(3))->method('dispatch')->willReturnCallback(function ($a) use ($recorder) { + if (2 === $a->idx) { + $message = new \stdClass(); + $message->idx = 3; + $recorder->record($message); + } + + return; + }); + + $next = new class($recorder) { + private $recorder; + + public function __construct(MessageRecorderInterface $recorder) + { + $this->recorder = $recorder; + } + + public function __invoke() + { + $message = new \stdClass(); + $message->idx = 1; + $this->recorder->record($message); + + $message = new \stdClass(); + $message->idx = 2; + $this->recorder->record($message); + } + }; + + $middleware = new HandleRecordedMessageMiddleware($messageBus, $recorder); + $middleware->handle($message, $next); + $this->assertEmpty($recorder->getRecordedMessages(), 'RecordedMessageContainerInterface should be empty after execution of HandleRecordedMessageMiddleware.'); + } + + public function testCatchExceptions() + { + $message = new DummyMessage('Hello'); + $recorder = new MessageRecorder(); + $messageBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $messageBus->expects($this->exactly(2))->method('dispatch')->willThrowException(new \LogicException('Foo')); + + $next = new class($recorder) { + private $recorder; + + public function __construct(MessageRecorderInterface $recorder) + { + $this->recorder = $recorder; + } + + public function __invoke() + { + $message = new \stdClass(); + $message->idx = 1; + $this->recorder->record($message); + + $message = new \stdClass(); + $message->idx = 2; + $this->recorder->record($message); + } + }; + + $middleware = new HandleRecordedMessageMiddleware($messageBus, $recorder); + $this->expectException(MessageHandlingException::class); + $this->expectExceptionMessage('Some handlers for recorded messages threw an exception. Their messages were: + +Foo, +Foo'); + $middleware->handle($message, $next); + } + + public function testItReturnsData() + { + $message = new DummyMessage('Hello'); + + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next->method('__invoke')->willReturn('World'); + + $messageBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $recorder = $this->getMockBuilder(RecordedMessageCollectionInterface::class)->getMock(); + + $middleware = new HandleRecordedMessageMiddleware($messageBus, $recorder); + + $result = $middleware->handle($message, $next); + + $this->assertEquals('World', $result); + } +} diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index a454f8f861701..62628ce74dca2 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -21,6 +21,7 @@ "require-dev": { "psr/log": "~1.0", "symfony/console": "~3.4|~4.0", + "symfony/contracts": "^1.0", "symfony/dependency-injection": "~3.4.6|~4.0", "symfony/http-kernel": "~3.4|~4.0", "symfony/process": "~3.4|~4.0",