Symfony Messenger is here, but your project is on Symfony 3.4, no problem! (part 1)

Hi, and yes, it’s not a joke, the new component of Symfony, Messenger, was released on 4.1 and also works well with the LTS 3.4.

Before starting, note that the component is still in experimental mode ⚠️

Requirements:

  • Symfony 3.4
  • PHP 7.1.3 min

Let’s make it work!

1) Installation

If your project uses Flex, you will need to bypass the plugin because Flex will try to use the recipe for the Messenger and this will not work with the FrameworkBundle of Symfony 3.4:

composer require symfony/messenger:"~4.2.0" --no-plugins

otherwise, just use this command:

composer require symfony/messenger:"~4.2.0"

2) CompilerPass

The component Messenger provides a compiler pass to simplify its configuration, activate it with:

via Kernel:

namespace App;

// ...

class Kernel extends BaseKernel
{
protected function configureContainer(
ContainerBuilder $container,
LoaderInterface $loader
) {
// ...

$container->addCompilerPass(new MessengerPass());
}
}

or via a bundle:

namespace AppBundle;

// ...

class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new MessengerPass());
}
}

3) Start configuration

We will create our first message bus: “my_super_bus”

Create a config file/config/packages/messenger.yaml with this content:

services:
_defaults:
autoconfigure: true
autowire: true
public: false

# Message Bus with it alias
my_super_bus:
class: Symfony\Component\Messenger\MessageBus
tags: ['messenger.bus']
arguments: [[]]

Symfony\Component\Messenger\MessageBusInterface: '@my_super_bus'

We declare a service, instance of MessageBus, with id my_super_bus
Test it with debug command:

❯ bin/console debug:autowiring MessageBusInterface
Autowirable Services
====================
The following classes & interfaces can be used as type-hints when autowiring:
(only showing classes/interfaces matching MessageBusInterface)
-------------------------------------------------
Symfony\Component\Messenger\MessageBusInterface
alias to my_super_bus
-------------------------------------------------

If you don’t see that, then your file is not imported automatically, you need to import it into a global configuration file with:

imports:
- { resource: relative/path/to/messenger.yaml }

Cool, we have our super bus 🚌 and it can be injected by Dependency Injection of Symfony (thanks autowiring ❤️) in another class or a controller action with:

/**
* @Route("/messenger")
*/
class ListPostController
{
public function __invoke(MessageBusInterface $bus): Response
{
// ...
}
}

but it is still like an empty box. Go to the next step for configuring messenger middlewares 🚀

4) Middlewares

The component Messenger works with middlewares. It provides a few, but we will focus on the two main ones:
 MessageHandleMiddleware and SendMessageMiddleware

With these, we can do some jobs in synchronous/asynchronous modes.

  1. Asynchronous mode, a scenario when the message A is captured by the SendMessageMiddleware and sent by one of its senders to other application or message queue:
--------------------------------------------------------------------
     A 
Bus ---> SendMessageMiddleware ---> HandleMessageMiddleware
\ A
—-—> - sender1 A
- sender2: ---> | Message `A` was sent by
- senderN | sender2

2. Synchronous mode, a scenario when the message A is captured by the HandleMessageMiddleware and processed by one of its handlers:

--------------------------------------------------------------------
    A(1)                        A(3)
Bus ----> SendMessageMiddleware ----> HandleMessageMiddleware
\ A(2) \ A(4)
-----> - sender1 | any —-—--> - handler1: <|
- sender2 | sender - handler2 |
- senderN | match, - handlerN |
| message A |
| returns back |
________________|
selected handler
takes the message

3. You can have another scenario when the message A is captured by two middlewares.

5) HandleMessageMiddleware

This middleware has a stack of handlers and its job is to find a handler for a current message.

Let’s configure it, put this code into messenger.yaml configuration file:

messenger.middleware.handle_message:
class: Symfony\Component\Messenger\Middleware\HandleMessageMiddleware
abstract: true
arguments: [[], true]

note: the second argument true disables throwing an exception if one message doesn’t have a registered handler in the stack.

Now, we must tell which middleware will be used by our super bus, do it with a DI parameter:

parameters:
my_super_bus.middleware:
- { id: handle_message }

note: this parameter will be used by the compiler pass (MessengerPass.php) and will be removed from the container after the compilation.

Let’s test it with this basic implementation of the message:

// Message class
class GetPostQuery
{
private $limit;
private $offset;

public function __construct(int $offset = 0, int $limit = 10)
{
$this->offset = $offset;
$this->limit = $limit;
}

public function getLimit(): int
{
return $this->limit;
}

public function getOffset(): int
{
return $this->offset;
}
}

and handler:

class GetPostHandler implements MessageHandlerInterface
{
public function __invoke(GetPostQuery $query): array
{
return array_fill(
$query->getOffset(),
$query->getLimit(),
[
'title' => 'Post title',
'date' => new \DateTime(),
]
);
}
}

This handler returns a simple array the dimension of which depends on the $query properties. Note the type hint of the __invoke method. The compiler pass (MessengerPass.php) will register in the stack this handler for all instances of GetPostQuery message dispatched in the bus.

Ah, don’t forget to define it as a tagged service:

# services.yaml

services:
# ...

App\Post\Handler\GetPostHandler:
tags:
- { name: 'messenger.message_handler', bus: 'my_super_bus' }

Cool, go test it with a simple controller:

/**
* @Route("/posts/list")
*/
class PostListController extends AbstractController
{
public function __invoke(MessageBusInterface $bus): Response
{
$envelop = $bus->dispatch(new GetPostQuery(0, 5));

/** @var HandledStamp $stamp */
if ($stamp = $envelop->last(HandledStamp::class)) {
$posts = $stamp->getResult();
}

return $this->render(
'post.html.twig',
['posts' => $posts ?? []]
);
}
}

Here is what appears on my browser (with the template twig):


In the next days, I will create the next part of this article to explain how to configure the asynchronous mode of Messenger with Symfony 3.4.

UP: 
* Here is the next part for the asynchronous mode
* You can find on my GitHub page the entire project-demo 🚀


If you encounter any issue with the component Messenger, please report it on GitHub.


Any feedback? Don’t hesitate to comment.