From 62023382607efd62127a291b351e208e2a84f964 Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Fri, 8 Nov 2019 11:04:47 +0100 Subject: [PATCH] [Messenger][Mailer] Send mails after the main message succeeded --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../FrameworkExtension.php | 9 ++ .../Resources/config/messenger.xml | 2 + .../FrameworkExtensionTest.php | 25 +++++- src/Symfony/Component/Mailer/CHANGELOG.md | 5 ++ .../SendMailAfterCurrentBusMiddleware.php | 52 +++++++++++ .../SendMailAfterCurrentBusMiddlewareTest.php | 87 +++++++++++++++++++ 7 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Mailer/Messenger/Middleware/SendMailAfterCurrentBusMiddleware.php create mode 100644 src/Symfony/Component/Mailer/Tests/Messenger/Middleware/SendMailAfterCurrentBusMiddlewareTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 8e1e69dfea111..51dc8adcee11c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 5.1.0 ----- + * Wired the `send_mail_after_current_bus` middleware by default in message buses, so mails are sent only after the main message succeeded * Added the `framework.router.context` configuration node to configure the `RequestContext` * Made `MicroKernelTrait::configureContainer()` compatible with `ContainerConfigurator` * Added a new `mailer.message_bus` option to configure or disable the message bus to use to send mails. diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 63727d8c1f6ce..eddb4ff7a5491 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -81,6 +81,7 @@ use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\Messenger\Middleware\SendMailAfterCurrentBusMiddleware; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; @@ -1581,6 +1582,7 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder 'before' => [ ['id' => 'add_bus_name_stamp_middleware'], ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'send_mail_after_current_bus'], ['id' => 'dispatch_after_current_bus'], ['id' => 'failed_message_processing_middleware'], ], @@ -1589,6 +1591,13 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ['id' => 'handle_message'], ], ]; + + if (!class_exists(SendMailAfterCurrentBusMiddleware::class)) { + $container->removeDefinition('messenger.middleware.send_mail_after_current_bus'); + $beforeMiddleware = &$defaultMiddleware['before']; + array_splice($beforeMiddleware, 2, 1); + } + foreach ($config['buses'] as $busId => $bus) { $middleware = $bus['middleware']; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index 14117ee8e40a4..ba4db5c275bf6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -44,6 +44,8 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 28dc65d714ae1..8335baeeb1dcc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -39,6 +39,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; +use Symfony\Component\Mailer\Messenger\Middleware\SendMailAfterCurrentBusMiddleware; use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -638,25 +639,41 @@ public function testMessengerWithMultipleBuses() $this->assertTrue($container->has('messenger.bus.commands')); $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); - $this->assertEquals([ + + $expectedMiddleware = [ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'send_mail_after_current_bus'], ['id' => 'dispatch_after_current_bus'], ['id' => 'failed_message_processing_middleware'], ['id' => 'send_message'], ['id' => 'handle_message'], - ], $container->getParameter('messenger.bus.commands.middleware')); + ]; + + if (!class_exists(SendMailAfterCurrentBusMiddleware::class)) { + array_splice($expectedMiddleware, 2, 1); + } + $this->assertEquals($expectedMiddleware, $container->getParameter('messenger.bus.commands.middleware')); + $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); - $this->assertEquals([ + + $expectedMiddleware = [ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'send_mail_after_current_bus'], ['id' => 'dispatch_after_current_bus'], ['id' => 'failed_message_processing_middleware'], ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], ['id' => 'send_message'], ['id' => 'handle_message'], - ], $container->getParameter('messenger.bus.events.middleware')); + ]; + + if (!class_exists(SendMailAfterCurrentBusMiddleware::class)) { + array_splice($expectedMiddleware, 2, 1); + } + $this->assertEquals($expectedMiddleware, $container->getParameter('messenger.bus.events.middleware')); + $this->assertTrue($container->has('messenger.bus.queries')); $this->assertSame([], $container->getDefinition('messenger.bus.queries')->getArgument(0)); $this->assertEquals([ diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 051984f00160c..1456de18418e2 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.1.0 +----- + + * Added `SendMailAfterCurrentBusMiddleware` to send mails from message buses only after the main message succeeded + 4.4.0 ----- diff --git a/src/Symfony/Component/Mailer/Messenger/Middleware/SendMailAfterCurrentBusMiddleware.php b/src/Symfony/Component/Mailer/Messenger/Middleware/SendMailAfterCurrentBusMiddleware.php new file mode 100644 index 0000000000000..2a3bb57dde2ac --- /dev/null +++ b/src/Symfony/Component/Mailer/Messenger/Middleware/SendMailAfterCurrentBusMiddleware.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Messenger\Middleware; + +use Symfony\Component\Mailer\Messenger\SendEmailMessage; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Middleware\StackInterface; +use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; + +/** + * Automatically adds a stamp to mail messages, so these are only dispatched once the main message handler(s) succeeded. + * It prevents sending mails despite the main process failed. + * MUST be registered before the dispatch_after_current_bus middleware so the stamp is taken into account. + * + * @see \Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware + * + * @author Maxime Steinhausser + */ +class SendMailAfterCurrentBusMiddleware implements MiddlewareInterface +{ + /** + * @var bool this property is used to signal if we are inside a the first/root call to + * MessageBusInterface::dispatch() or if dispatch has been called inside a message handler + */ + private $isRootDispatchCallRunning = false; + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + try { + if ($this->isRootDispatchCallRunning && $envelope->getMessage() instanceof SendEmailMessage) { + $envelope = $envelope->with(new DispatchAfterCurrentBusStamp()); + } + + // First time we get here, mark as inside a "root dispatch" call: + $this->isRootDispatchCallRunning = true; + + return $stack->next()->handle($envelope, $stack); + } finally { + $this->isRootDispatchCallRunning = false; + } + } +} diff --git a/src/Symfony/Component/Mailer/Tests/Messenger/Middleware/SendMailAfterCurrentBusMiddlewareTest.php b/src/Symfony/Component/Mailer/Tests/Messenger/Middleware/SendMailAfterCurrentBusMiddlewareTest.php new file mode 100644 index 0000000000000..3cc93a5ae5dc8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Messenger/Middleware/SendMailAfterCurrentBusMiddlewareTest.php @@ -0,0 +1,87 @@ +createMock(MiddlewareInterface::class); + + $bus = new MessageBus([ + $middleware, + new DispatchAfterCurrentBusMiddleware(), + $dispatchingMiddleware = new DispatchingMiddleware([ + $sendMail, + ]), + $handlingMiddleware, + ]); + + $dispatchingMiddleware->setBus($bus); + + // Expect main dispatched message to be handled first: + $this->expectHandledMessage($handlingMiddleware, 0, $message); + // Then, expect mail to be sent: + $this->expectHandledMessage($handlingMiddleware, 1, $sendMail); + + $bus->dispatch($message); + } + + /** + * @param MiddlewareInterface|MockObject $handlingMiddleware + */ + private function expectHandledMessage(MiddlewareInterface $handlingMiddleware, int $at, $message): void + { + $handlingMiddleware->expects($this->at($at))->method('handle')->with($this->callback(function (Envelope $envelope) use ($message) { + return $envelope->getMessage() === $message; + }))->willReturnCallback(function ($envelope, StackInterface $stack) { + return $stack->next()->handle($envelope, $stack); + }); + } +} + +class DummyMessage +{ +} + +class DispatchingMiddleware implements MiddlewareInterface +{ + /** @var MessageBusInterface */ + private $bus; + private $messages; + + public function __construct(array $messages) + { + $this->messages = $messages; + } + + public function setBus(MessageBusInterface $bus): void + { + $this->bus = $bus; + } + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + while ($message = array_shift($this->messages)) { + $this->bus->dispatch($message); + } + + return $stack->next()->handle($envelope, $stack); + } +}