Skip to content

Commit cf814cf

Browse files
authored
Explain HandleMessageInNewTransaction middleware
The current PR is for HandleMessageInNewTransaction middleware instead of the original RecordsMessages middleware. This documentation covers the new middleware.
1 parent aab81e4 commit cf814cf

File tree

1 file changed

+72
-29
lines changed

1 file changed

+72
-29
lines changed

messenger/message-recorder.rst

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,62 @@
11
.. index::
2-
single: Messenger; Record messages
2+
single: Messenger; Record messages; Transaction messages
33

4-
Events Recorder: Handle Events After CommandHandler Is Done
5-
===========================================================
4+
Transactional Messages: Handle Events After CommandHandler Is Done
5+
==================================================================
66

7-
Let's take the example of an application that has a command (a CQRS message) named
8-
``CreateUser``. That command is handled by the ``CreateUserHandler`` which creates
9-
a ``User`` object, stores that object to a database and dispatches a ``UserCreated`` event.
10-
That event is also a normal message but is handled by an *event* bus.
7+
A message handler can ``dispatch`` new messages during execution, to either the same or
8+
a different bus (if the application has `multiple buses </messenger/multiple_buses>`_).
9+
Any errors or exceptions that occur during this process can have unintended consequences,
10+
such as:
1111

12-
There are many subscribers to the ``UserCreated`` event, one subscriber may send
12+
- If using the ``DoctrineTransactionMiddleware`` and a dispatched message to the same bus
13+
and an exception is thrown, then any database transactions in the original handler will
14+
be rolled back.
15+
- If the message is dispatched to a different bus, then the dispatched message can still
16+
be handled even if the original handler encounters an exception.
17+
18+
An Example ``SignUpUser`` Process
19+
---------------------------------
20+
21+
Let's take the example of an application with both a *command* and an *event* bus. The application
22+
dispatches a command named ``SignUpUser`` to the command bus. The command is handled by the
23+
``SignUpUserHandler`` which creates a ``User`` object, stores that object to a database and
24+
dispatches a ``UserSignedUp`` event to the event bus.
25+
26+
There are many subscribers to the ``UserSignedUp`` event, one subscriber may send
1327
a welcome email to the new user. We are using the ``DoctrineTransactionMiddleware``
1428
to wrap all database queries in one database transaction.
1529

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

20-
**Solution:** The solution is to not dispatch the ``UserCreated`` event in the
21-
``CreateUserHandler`` but to just "record" the events. The recorded events will
22-
be dispatched after ``DoctrineTransactionMiddleware`` has committed the transaction.
34+
**Problem 2:** If an exception is thrown when saving the user to the database, the welcome
35+
email is still sent.
36+
37+
``HandleMessageInNewTransaction`` Middleware
38+
--------------------------------------------
39+
40+
For many applications, the desired behavior is to have any messages dispatched by the handler
41+
to `only` be handled after the handler finishes. This can be by using the
42+
``HandleMessageInNewTransaction`` middleware and adding a ``Transaction`` stamp to
43+
`the message Envelope </components/messenger#adding-metadata-to-messages-envelopes>`_.
44+
This middleware enables us to add messages to a separate transaction that will only be
45+
dispatched *after* the current message handler finishes.
2346

24-
To enable this, you simply just add the ``messenger.middleware.handles_recorded_messages``
47+
Referencing the above example, this means that the ``UserSignedUp`` event would not be handled
48+
until *after* the ``SignUpUserHandler`` had completed and the new ``User`` was persisted to the
49+
database. If the ``SignUpUserHandler`` encounters an exception, the ``UserSignedUp`` event will
50+
never be handled and if an exception is thrown while sending the welcome email, the Doctrine
51+
transaction will not be rolled back.
52+
53+
To enable this, you need to add the ``handle_message_in_new_transaction``
2554
middleware. Make sure it is registered before ``DoctrineTransactionMiddleware``
2655
in the middleware chain.
2756

57+
**Note:** The ``handle_message_in_new_transaction`` middleware must be loaded for *all* of the
58+
buses. For the example, the middleware must be loaded for both the command and event buses.
59+
2860
.. configuration-block::
2961

3062
.. code-block:: yaml
@@ -33,45 +65,56 @@ in the middleware chain.
3365
framework:
3466
messenger:
3567
default_bus: messenger.bus.command
68+
3669
buses:
3770
messenger.bus.command:
3871
middleware:
39-
- messenger.middleware.validation
40-
- messenger.middleware.handles_recorded_messages: ['@messenger.bus.event']
41-
# Doctrine transaction must be after handles_recorded_messages middleware
42-
- app.doctrine_transaction_middleware: ['default']
72+
- validation
73+
- handle_message_in_new_transaction
74+
- doctrine_transaction
4375
messenger.bus.event:
76+
default_middleware: allow_no_handlers
4477
middleware:
45-
- messenger.middleware.allow_no_handler
46-
- messenger.middleware.validation
78+
- validation
79+
- handle_message_in_new_transaction
80+
- doctrine_transaction
81+
4782
4883
.. code-block:: php
4984
5085
namespace App\Messenger\CommandHandler;
5186
5287
use App\Entity\User;
53-
use App\Messenger\Command\CreateUser;
54-
use App\Messenger\Event\UserCreatedEvent;
88+
use App\Messenger\Command\SignUpUser;
89+
use App\Messenger\Event\UserSignedUp;
5590
use Doctrine\ORM\EntityManagerInterface;
56-
use Symfony\Component\Messenger\MessageRecorderInterface;
91+
use Symfony\Component\Messenger\Envelope;
92+
use Symfony\Component\Messenger\Stamp\Transaction;
93+
use Symfony\Component\Messenger\MessageBusInterface;
5794
58-
class CreateUserHandler
95+
class SignUpUserHandler
5996
{
6097
private $em;
61-
private $eventRecorder;
98+
private $eventBus;
6299
63-
public function __construct(MessageRecorderInterface $eventRecorder, EntityManagerInterface $em)
100+
public function __construct(MessageBusInterface $eventBus, EntityManagerInterface $em)
64101
{
65-
$this->eventRecorder = $eventRecorder;
102+
$this->eventBus = $eventBus;
66103
$this->em = $em;
67104
}
68105
69-
public function __invoke(CreateUser $command)
106+
public function __invoke(SignUpUser $command)
70107
{
71108
$user = new User($command->getUuid(), $command->getName(), $command->getEmail());
72109
$this->em->persist($user);
73110
74-
// "Record" this event to be processed later by "handles_recorded_messages".
75-
$this->eventRecorder->record(new UserCreatedEvent($command->getUuid());
111+
// The Transaction stamp marks the event message to be handled
112+
// only if this handler does not throw an exception.
113+
114+
$event = new UserSignedUp($command->getUuid());
115+
$this->eventBus->dispatch(
116+
(new Envelope($event))
117+
->with(new Transaction())
118+
);
76119
}
77120
}

0 commit comments

Comments
 (0)