Компонент Messenger (Symfony)

Back in March 2018, the pleasing news that the family of symfony components was replenished with a useful Messenger. tool spread quickly among the developers. According to the creators, this Symfony tool had to take on the task of sending / receiving messages to / from other applications, as well as queue management.

Naturally, the appearance of an interesting tool causes a desire to implement it quickly in projects. However, few people can afford it. Therefore, we did not risk our time, as well as stability of our customers’ applications, and postponed the question.

And now, 2020 has arrived, and every time I look through the Changelog of the new Symfony releases, I run into the Messenger edits. It’s obvious that the work has been in full swing for almost two years, the component seems to be more stable already, which means it’s time to take a closer look at it.

I don’t like to copy examples from documentation, so I decided to focus on conceptual things myself, and to study the structure of the Messenger, its basic abstractions and the tasks assigned to them.

The difficulty of perception

I’ll start with a small lyrical digression and touch on the topic of the complexity of information perception.

The complexity of studying a new topic to a great extent depends on how the author presents the material (under the term material I mean: documentation, articles, screencast, etc.). You have probably heard, or possibly met, the people who are real professionals in their field, but who are absolutely incapable of expressing themselves in a simple way, and sometimes their attempts to do this only make the situation worse.

The knowledge transfer is not a completely formalized process, that is, there is no special approach in which every student will understand the material quickly and correctly. Therefore, everyone does it the way they can.

I decided to write about it, because the first thing I came across when I started to study Messenger was the block diagram presented below and despite the fact that schemes should simplify the perception, it caused the opposite effect on me.

Source: https://symfony.com/doc/current/components/messenger.html

For you not to spend much time, I would like to make some thesis recommendations on reading this scheme:

    • — Your message, and it can be any object, turns into an additional wrapper called Envelope. You can see this by looking at the implementation of the dispatch method of the MessageBus class.
      1. $envelope = Envelope::wrap($message, $stamps);
    • Inside the Bus, Envelope passes through a chain of Middlewares with the help of which additional logic can be used (it’s not reflected in the diagram);
    • Stamps — this is the metadata which allows you to actually manage your message inside Middlewares (it’s not reflected in the diagram). The principle is extremely simple if Envelope is sent with some kind of Stamp, knowing it we can perform additional logic;
    • Having passed the Envelope through all Middlewares, the Bus Bus uses the Transport you specified and finally sends a message;
    • The diagram shows that the Receiver, Bus and Sender have the same color. In my opinion, this is not exactly correct. The Receiver and Sender are Transport.

You can make it sure by studying the structure of the Message component a little.

Thus, under the phrase «the Bus uses Transport», it is assumed that the Sender of the specified Transport will be activated for your message. A similar conclusion applies to the Receiver; — The circles on the diagram mark specifically the 3rd part systems such as RabbitMQ, Doctrine, Redis, etc.

I would be glad if these notes help you to gain insight into the scheme quickly. I succeeded in it only after I had studied the codebase and examples from the documentation.

Message Bus

It’s a basic abstraction the tasks of which include control over the process of sending a message and its transfer to the handler. Despite the fact that the tasks assigned to the Bus are the key ones, it is a fairly simple class with one dispatch method. The thing is that Bus does it in a very elegant way.

As you can see from the code, the Bus performs only two key functions: it wraps the message into the Envelope (more details about this abstraction later) and passes the message iteratively through Middlewares.

Middlewares are the very building blocks that implement Messenger’s basic logic. It is worth knowing that Middlewares are fulfilled according to the order in which they are registered in the container.

Of course, you can also implement any business logic in your Middlewares, but keep in mind that SendMessageMiddleware and HandleMessageMiddleware are always executed last ... which is generally logical :)

The list available can be obtained by running the following command:

    php bin/console debug:container messenger.middleware

At the time of article writing, it’s:

     [0 ] messenger.middleware.send_message
      [1 ] messenger.middleware.handle_message
      [2 ] messenger.middleware.add_bus_name_stamp_middleware
      [3 ] messenger.middleware.dispatch_after_current_bus
      [4 ] messenger.middleware.validation
      [5 ] messenger.middleware.reject_redelivered_message_middleware
      [6 ] messenger.middleware.failed_message_processing_middleware
      [7 ] messenger.middleware.traceable
      [8 ] messenger.middleware.doctrine_transaction
      [9 ] messenger.middleware.doctrine_ping_connection
      [10] messenger.middleware.doctrine_close_connection

Middleware, Envelope and Stamps

An object, as an option for transmitting a message, is chosen for a reason, it is a great way to transfer context to the other parts of the system.

What does context mean in this case? This is the set of data that is needed to process a message. For example, we need to send a greeting letter after a new user is registered. We can create a UserRegisteredMessage message class and store the UUID in it as a context. The Handler will find this UUID of the user in the database and send him a letter to the mailing address.

    class UserRegisteredMessage
    {
       /** @var string */
       private $userId;
     
       /**
        * @param string $userId
        */
       public function __construct(string $userId)
       {
           $this->userId = $userId;
       }
     
       /**
        * @return string
        */
       public function getUserId(): string
       {
           return $this->userId;
       }
    }

It is easy to understand and clear from EventDispatcher. What are the reasons for the appearance of the Envelope and Stamp abstractions then?

I have already mentioned that Bus passes a message through the intermediate logic, the so-called Middlewares. We can manage our message within each Middleware.

Now the question arises if I want Middleware 1 to do nothing with the message, and some logic to be fulfilled in Middleware 2, how can I notify Middleware 2 about it? (see the diagram below).

An option is to check the instance of the transmitted message inMiddleware 2.

    $envelope instanceof UserRegisteredMessage

This is a good solution, but then Middleware 2 will be tightly associated with a specific message. I do not exclude that such cases can also be, but what if we are dealing with a universal Middleware that must process different messages?

Here we come to the idea of metadata, that is, some kind of auxiliary data set that should be sent along with the message. Stamps does it This is a marker that carries its own context.

And this is the root cause of the Stamps. As for Envelope, you need to answer the following question: how correctly is it to transfer metadata along with the message context? I mean, save Stamps directly in the message object.

The answer is the following: it will be a very gross violation, because the message context comes out of the business requirements while the metadata out of the implementation.

This leads to the second idea that contexts need to be transferred as separate objects, and to control them, you need an auxiliary abstraction, a certain control object. This function was assigned just to Envelope. He acts as a wrapper for our post and adds the ability to bind Stamps to a message.

Let’s consolidate our understanding of the concept with a simple example. Suppose you want to validate a message before sending it to a handler. This problem can be solved with the help of ValidationMiddleware available from the box. Pay attention to the 39th line, the validator will work if the ValidationStamp was attached to Envelope

    if ($validationStamp = $envelope->last(ValidationStamp::class)) {
        $groups = $validationStamp->getGroups();
    }

To do this, during dispatch, we will pass a ValidationStamp object with the name of the validation group. And then in case of an error, a ValidationFailedException will be dropped, which can be handled by the caller.

    $bus->dispatch($message), [
       new ValidationStamp([
           'validation_group',
       ])
    ]);

Transport, Sender, and Receiver

For a better understanding of what the Transport is, it’s better to think of it as of some kind of repository for working with which two more abstractions are needed, the Sender, which implements sending a message to the repository, and the Receiver, which takes this message from the repository and sends it to the handler.

Out of the box, we have three asynchronous Transports: AmqpExt, RedisExt and Doctrine, as well as two synchronous ones: In Memory and Sync. Moreover, the implementation of the Receiver is not required for Sync since the Messages are immediately sent to Handler. In general, it does the same as EventDispatcher (well, you’ve got it, have you ;)).

Of course, you have the opportunity to create your own custom Transport. For this, the two TransportFactory and TransportInterface interfaces must be implemented.

The aspect of Transports can take one more full article, because there are a lot of questions, ranging from configuration and queue management to the worker and deployment operating. But since the purpose of the article is to describe the basic abstractions and their tasks; I will stick to a brief review of this topic.

Conclusions

The conclusion is clear, Messenger is a useful symfony development tool and we will definitely use it in our company future projects.

In fact, we get an improved EventDispatcher: with routing, Middlewares, the ability to control the message through Stamps, and also to use various Transport, including asynchronous.

Drawing such conclusions, I have, in fact, put Messenger on a par with EventDispatcher, but I want to emphasize that I am aware of the difference in the approaches these two components use.

EventDispatcher dispatches, and Listeners / Subscribers process. At the same time, the Messenger both sends and controls processing. So we can summarize that Messenger can do everything that EventDispatcher does and even more, but as to the EventDispatcher it’s wrong to say the same, since it is limited compared to the first tool.

You have the right to decide yourself what to use, but I would prefer Messenger as a more flexible alternative.

Now let’s see some statistics. At the time of writing the article, the packagist.org stated:

1.8 million installations and almost 500 stars.

As to the issues we have 41/190 (opened / closed) of which:

    • bugs: 13/93;
    • performance: 2/5;
    • feature: 31/160.

As you can see, the community is actively interested and works to improve stability of the component, as well as adds new features.

And finally, in the process of studying the materials available on the Internet, I came across a rather interesting article on the topic. I am not familiar with the author, but I am very impressed with the way the information presented, so I recommend you to read it

https://vria.eu/delve_into_the_heart_of_the_symfony_messenger/

Well, and now have the colorful Cheat Sheet :)

https://medium.com/@andreiabohner/symfony-4-4-messenger-cheat-sheet-5ca99cbba4a8

Thank you for reading up to the end. Good luck!