Skip to content

Commit

Permalink
feature #10015 [Messenger] Added documentation about DispatchAfterCur…
Browse files Browse the repository at this point in the history
…rentBusMiddleware (Nyholm, gubler, OskarStark)

This PR was submitted for the master branch but it was merged into the 4.3 branch instead (closes #10015).

Discussion
----------

[Messenger] Added documentation about DispatchAfterCurrentBusMiddleware

Documentation to PR: symfony/symfony#27844

![Screenshot 2019-03-17 at 11 43 23](https://user-images.githubusercontent.com/1275206/54489171-edee4880-48a9-11e9-8028-ac501e62dfcb.png)
![Screenshot 2019-03-17 at 11 43 34](https://user-images.githubusercontent.com/1275206/54489173-edee4880-48a9-11e9-877f-f749f242b502.png)

![Screenshot 2019-03-17 at 11 43 39](https://user-images.githubusercontent.com/1275206/54489172-edee4880-48a9-11e9-84f1-d78e68474c7a.png)

Commits
-------

0d12880 minor updates according to feedback
ec3e1d7 updates according to feedback
3e99663 Update messenger/message-recorder.rst
fe81abc Update messenger/message-recorder.rst
c1cef9e Update messenger/message-recorder.rst
cfa56f7 Update messenger/message-recorder.rst
06ec8a4 Added PHP and XML config
797f530 Minor updates
30e958c Explain HandleMessageInNewTransaction middleware
84b81e3 Added fixes
ca22c3c a user
fff8659 According to feedback
afdddbf Added documentation about message recorder
  • Loading branch information
weaverryan committed May 24, 2019
2 parents cde5dde + 0d12880 commit 5e1033a
Showing 1 changed file with 192 additions and 0 deletions.
192 changes: 192 additions & 0 deletions messenger/message-recorder.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
.. index::
single: Messenger; Record messages; Transaction messages

Transactional Messages: Handle Events After CommandHandler is Done
==================================================================

A message handler can ``dispatch`` new messages during execution, to either the same or
a different bus (if the application has `multiple buses </messenger/multiple_buses>`_).
Any errors or exceptions that occur during this process can have unintended consequences,
such as:

- If using the ``DoctrineTransactionMiddleware`` and a dispatched message throws an exception,
then any database transactions in the original handler will be rolled back.
- If the message is dispatched to a different bus, then dispatched message will be
handled even if the current handler throws an exception.

An Example ``RegisterUser`` Process
-----------------------------------

Let's take the example of an application with both a *command* and an *event* bus. The application
dispatches a command named ``RegisterUser`` to the command bus. The command is handled by the
``RegisterUserHandler`` which creates a ``User`` object, stores that object to a database and
dispatches a ``UserRegistered`` event to the event bus.

There are many subscribers to the ``UserRegistered`` event, one subscriber may send
a welcome email to the new user. We are using the ``DoctrineTransactionMiddleware``
to wrap all database queries in one database transaction.

**Problem 1:** If an exception is thrown when sending the welcome email, then the user
will not be created because the ``DoctrineTransactionMiddleware`` will rollback the
Doctrine transaction, in which the user has been created.

**Problem 2:** If an exception is thrown when saving the user to the database, the welcome
email is still sent because it is handled asynchronously.

``DispatchAfterCurrentBusMiddleware`` Middleware
------------------------------------------------

For many applications, the desired behavior is to have any messages dispatched by the handler
to `only` be handled after the handler finishes. This can be by using the
``DispatchAfterCurrentBusMiddleware`` middleware and adding a ``DispatchAfterCurrentBusStamp``
stamp to `the message Envelope </components/messenger#adding-metadata-to-messages-envelopes>`_.

Referencing the above example, this means that the ``UserRegistered`` event would not be handled
until *after* the ``RegisterUserHandler`` had completed and the new ``User`` was persisted to the
database. If the ``RegisterUserHandler`` encounters an exception, the ``UserRegistered`` event will
never be handled and if an exception is thrown while sending the welcome email, the Doctrine
transaction will not be rolled back.

The ``dispatch_after_current_bus`` middleware is enabled by default. It is configured as the
first middleware on all busses. When doing a highly custom or special configuration, then make
sure ``dispatch_after_current_bus`` is registered before ``doctrine_transaction``
in the middleware chain.

**Note:** The ``dispatch_after_current_bus`` middleware must be loaded for *all* of the
buses. For the example, the middleware must be loaded for both the command and event bus.

.. configuration-block::

.. code-block:: yaml
# config/packages/messenger.yaml
framework:
messenger:
default_bus: messenger.bus.command
buses:
messenger.bus.command:
middleware:
- validation
messenger.bus.event:
default_middleware: allow_no_handlers
middleware:
- validation
.. code-block:: xml
<!-- config/packages/messenger.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<framework:config>
<framework:messenger default_bus="messenger.bus.command">
<framework:bus name="messenger.bus.command">
<framework:middleware id="validation">
<framework:middleware id="doctrine_transaction">
</framework:bus>
<framework:bus name="messenger.bus.command" default_middleware="allow_no_handlers">
<framework:middleware id="validation">
<framework:middleware id="doctrine_transaction">
</framework:bus>
</framework:messenger>
</framework:config>
</container>
.. code-block:: php
// config/packages/messenger.php
$container->loadFromExtension('framework', [
'messenger' => [
'default_bus' => 'messenger.bus.command',
'buses' => [
'messenger.bus.command' => [
'middleware' => ['validation', 'doctrine_transaction'],
],
'messenger.bus.event' => [
'default_middleware' => 'allow_no_handlers',
'middleware' => ['validation', 'doctrine_transaction'],
],
],
],
]);
.. code-block:: php
namespace App\Messenger\CommandHandler;
use App\Entity\User;
use App\Messenger\Command\RegisterUser;
use App\Messenger\Event\UserRegistered;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp;
use Symfony\Component\Messenger\MessageBusInterface;
class RegisterUserHandler
{
private $eventBus;
private $em;
public function __construct(MessageBusInterface $eventBus, EntityManagerInterface $em)
{
$this->eventBus = $eventBus;
$this->em = $em;
}
public function __invoke(RegisterUser $command)
{
$user = new User($command->getUuid(), $command->getName(), $command->getEmail());
$this->em->persist($user);
// The DispatchAfterCurrentBusStamp marks the event message to be handled
// only if this handler does not throw an exception.
$event = new UserRegistered($command->getUuid());
$this->eventBus->dispatch(
(new Envelope($event))
->with(new DispatchAfterCurrentBusStamp())
);
// ...
}
}
.. code-block:: php
namespace App\Messenger\EventSubscriber;
use App\Entity\User;
use App\Messenger\Event\UserRegistered;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\RawMessage;
class WhenUserRegisteredThenSendWelcomeEmail
{
private $mailer;
private $em;
public function __construct(MailerInterface $mailer, EntityManagerInterface $em)
{
$this->mailer = $mailer;
$this->em = $em;
}
public function __invoke(UserRegistered $event)
{
$user = $this->em->getRepository(User::class)->find(new User($event->getUuid()));
$this->mailer->send(new RawMessage('Welcome '.$user->getFirstName()));
}
}
.. note::

If ``WhenUserRegisteredThenSendWelcomeEmail`` throws an exception, that exception
will be wrapped into a ``DelayedMessageHandlingException``. Using ``DelayedMessageHandlingException::getExceptions``
will give you all exceptions that are thrown while handing a message with the ``DispatchAfterCurrentBusStamp``.

0 comments on commit 5e1033a

Please sign in to comment.