Skip to content

Command Bus🔗

Installation🔗

1
composer require symfony/messenger

See documentation.

Configuration🔗

Create the buses:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# config/packages/messenger.yaml
framework:
    messenger:
        buses:
            messenger.bus.commands:
                middleware:
                    - validation
                    - doctrine_transaction
            messenger.bus.queries:
                middleware:
                    - validation

Note: I recommend using at least the validation middleware on both buses to validate your commands/queries and the doctrine_transaction middleware on the command bus in order to wrap your handler into a transaction that will be rollback in case of exception.

Tag commands and queries:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
    query_handlers:
        namespace: App\Application\
        resource: '../src/Application/**/Query/*QueryHandler.php'
        public: true
        tags:
            - { name: 'messenger.message_handler', bus: 'messenger.bus.queries' }
    command_handlers:
        namespace: App\Application\
        resource: '../src/Application/**/Command/*CommandHandler.php'
        public: true
        tags:
            - { name: 'messenger.message_handler', bus: 'messenger.bus.commands' }

Note: Change resource path and namespace according to your project

Then create a bus using the dedicated HandleTrait:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Infra\Bus;

use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;

class Bus
{
    use HandleTrait { handle as public handle; }

    /** @var MessageBusInterface */
    private $messageBus;

    public function __construct(MessageBusInterface $commandMessageBus)
    {
        $this->messageBus = $commandMessageBus;
    }
}

And register query bus and the command bus services:

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    query_bus:
        class: App\Infra\Bus\Bus
        arguments: ['@messenger.bus.queries']
    command_bus:
        class: App\Infra\Bus\Bus
        arguments: ['@messenger.bus.com']

Autowire🔗

In order to autowire your buses, you have two options:

Variables binding🔗

Bind the two buses to variables names:

1
2
3
4
5
6
# config/services.yaml
services:
    _defaults:
        bind:
            $queryBus: '@query_bus'
            $commandBus: '@command_bus'

Then:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

class FoobarController
{
    private Bus $commandBus;
    private Bus $queryBus;

    public function __construct(Bus $commandBus, Bus $queryBus)
    {
        $this->commandBus = $commandBus;
        $this->queryBus = $queryBus;
    }
}

Type-hinting🔗

If you prefer, you can create two classes or two interfaces for query and command bus in order to use type-hinting for autowire instead of binding variables.

For example with interfaces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php

namespace App\Infra\Bus;

use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;

interface CommandBus { public function handle($command); }
interface QueryBus { public function handle($query); }

class Bus implements CommandBus, QueryBus
{
   // Same as before ...
}

Register query bus and the command bus services and aliases:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# config/services.yaml
services:
    query_bus:
        class: App\Infra\Bus\Bus
        arguments: ['@messenger.bus.queries']
    command_bus:
        class: App\Infra\Bus\Bus
        arguments: ['@messenger.bus.com']
    App\Infra\Bus\CommandBus: '@command_bus'
    App\Infra\Bus\QueryBus: '@query_bus'

Then:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

class FoobarController
{
    private CommandBus $commandBus;
    private QueryBus $queryBus;

    public function __construct(CommandBus $commandBus, QueryBus $queryBus)
    {
        $this->commandBus = $commandBus;
        $this->queryBus = $queryBus;
    }
}

Unwrap exceptions🔗

Exception thrown from an handler will be wrapped in HandlerFailedException.

In your bus🔗

If you want to be able to catch your exception instead of HandlerFailedException in your controller for example, you can unwrap exceptions in the bus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php

class Bus implements CommandBus, QueryBus
{
    use HandleTrait { handle as doHandle; }

    public function handle($message)
    {
        try {
            return $this->doHandle($message);
        } catch (HandlerFailedException $exception) {
            throw $exception->getPrevious();
        }
    }
}

In the layer's exception listener🔗

Directly in a kernel.exception listener to handle the exception for the HTTP layer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

namespace App\Infra\Common\Listener;

use App\Domain\Common\Exception\ForbiddenException;
use App\Domain\Common\Exception\NotFoundException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Messenger\Exception\HandlerFailedException;

/**
 * Converts some of our custom exceptions to HTTP ones to be handled by the framework.
 */
class ToHttpExceptionListener implements EventSubscriberInterface
{
    public function onKernelException(ExceptionEvent $event): void
    {
        $ex = $event->getThrowable();

        // An exception thrown from a Messenger handler is wrapped into a HandlerFailedException:
        if ($ex instanceof HandlerFailedException) {
            $ex = $ex->getPrevious();
        }

        switch (true) {
            case $ex instanceof NotFoundException:
                $event->setThrowable(new NotFoundHttpException($ex->getMessage(), $ex));

                return;
            case $ex instanceof ForbiddenException:
                $event->setThrowable(new AccessDeniedHttpException($ex->getMessage(), $ex));

                return;

            // Or any other logic for your app
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::EXCEPTION => ['onKernelException', 10],
        ];
    }
}

Using a middleware🔗

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Infra\Bridge\Symfony\Messenger\Middleware;

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;

class UnwrapExceptionMiddleware implements MiddlewareInterface
{
    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        try {
            return $stack->next()->handle($envelope, $stack);
        } catch (HandlerFailedException $exception) {
            throw $exception->getPrevious();
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# config/packages/messenger.yaml
framework:
    messenger:
        buses:
            messenger.bus.commands:
                middleware:
                    - App\Infra\Bus\UnwrapExceptionMiddleware
                    - validation
                    - doctrine_transaction
            messenger.bus.queries:
                middleware:
                    - App\Infra\Bus\UnwrapExceptionMiddleware
                    - validation

Project references🔗


Last update: December 20, 2024