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);
+ }
+}