Skip to content

[Messenger][Mailer] Send mails after the main message succeeded #34291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'],
],
Expand All @@ -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'];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

<service id="messenger.middleware.dispatch_after_current_bus" class="Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware" />

<service id="messenger.middleware.send_mail_after_current_bus" class="Symfony\Component\Mailer\Messenger\Middleware\SendMailAfterCurrentBusMiddleware" />

<service id="messenger.middleware.validation" class="Symfony\Component\Messenger\Middleware\ValidationMiddleware">
<argument type="service" id="validator" />
</service>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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([
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Mailer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
-----

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?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\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 <maxime.steinhausser@gmail.com>
*/
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace Symfony\Component\Mailer\Tests\Messenger\Middleware;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Messenger\Middleware\SendMailAfterCurrentBusMiddleware;
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Mime\RawMessage;

class SendMailAfterCurrentBusMiddlewareTest extends TestCase
{
public function testMailSentAfterMainMessage()
{
$message = new DummyMessage();
$sendMail = new SendEmailMessage(new RawMessage('foo'));

$middleware = new SendMailAfterCurrentBusMiddleware();
$handlingMiddleware = $this->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);
}
}