CQRS is easy with Symfony 4 and his Messenger Component

Today i want to show you how to use The Messenger Component of Symfony.
Very useful when your project implements the CQRS pattern.

With an example, simplicity of use and practicality will be evident.

Let’s assume that we have a service like this one:

<?php

namespace App\Domain\Service\Customer;

use App\Domain\Command\Customer\DeleteCustomerCommand;
use App\Domain\CommandHandler\Customer\DeleteCustomerCommandHandlerInterface;
use App\Domain\Exception\Customer\CriteriaNotAllowedException;
use App\Domain\Query\Customer\GetCustomerListQuery;
use App\Domain\Query\Customer\GetCustomerQuery;
use App\Domain\QueryHandler\Customer\GetCustomerListQueryHandlerInterface;
use App\Domain\QueryHandler\Customer\GetCustomerQueryHandlerInterface;

class CustomerService implements CustomerServiceInterface
{
    /**
     * @var GetCustomerListQueryHandlerInterface
     */
    private $customerListQueryHandler;
    /**
     * @var GetCustomerQueryHandlerInterface
     */
    private $customerQueryHandler;
    /**
     * @var DeleteCustomerCommandHandlerInterface
     */
    private $deleteCustomerHandler;

    public function __construct(
        GetCustomerListQueryHandlerInterface $customerListQueryHandler,
        GetCustomerQueryHandlerInterface $customerQueryHandler,
        DeleteCustomerCommandHandlerInterface $deleteCustomerHandler
    ) {
        $this->customerListQueryHandler = $customerListQueryHandler;
        $this->customerQueryHandler = $customerQueryHandler;
        $this->deleteCustomerHandler = $deleteCustomerHandler;
    }

    /**
     * @param array $criteria
     *
     * @return mixed
     *
     * @throws CriteriaNotAllowedException
     */
    public function getByCriteria(array $criteria)
    {
        $getCustomerQueryList = new GetCustomerListQuery();

        foreach ($criteria as $key => $value) {
            $method = 'set' . ucfirst($key);

            if (!method_exists($getCustomerQueryList, $method)) {
                throw new CriteriaNotAllowedException(sprintf('Parameter %s not allowed', $key));
            }

            $getCustomerQueryList->$method($value);
        }

        return $this->customerListQueryHandler->handle($getCustomerQueryList);
    }

    public function get(string $id)
    {
        return $this->customerQueryHandler->handle(
            new GetCustomerQuery($id)
        );
    }

    public function delete(string $id)
    {
        $this->deleteCustomerHandler->handle(
            new DeleteCustomerCommand($id)
        );
    }
}


A very simple service that is used to search users by criteria and to get or remove a specific user.

To correctly apply the CQRS pattern, I had to inject in the service the Query and Command classes, and the interfaces that are implemented by the handlers (yes, because we also apply the concept of hexagonal architecture).

Now, if we use the Messenger Component of Symfony

  1. Install component:
    composer require symfony/messenger

     

  2. Configure component into config/services.yaml:
    App\Infrastructure\QueryHandler\:
        resource: '../src/Infrastructure/QueryHandler/*'
        public: true
        tags: [messenger.message_handler]
    App\Infrastructure\CommandHandler\:
        resource: '../src/Infrastructure/CommandHandler/*'
        public: true
        tags: [messenger.message_handler]

    Be careful not to forget the tag messenger.message_handler

  3.  Change the service as follows:
    <?php
    
    namespace App\Domain\Service\Customer;
    
    use App\Domain\Command\Customer\DeleteCustomerCommand;
    use App\Domain\Exception\Customer\CriteriaNotAllowedException;
    use App\Domain\Query\Customer\GetCustomerListQuery;
    use App\Domain\Query\Customer\GetCustomerQuery;
    use Symfony\Component\Messenger\MessageBusInterface;
    
    class CustomerService implements CustomerServiceInterface
    {
        /** @var MessageBusInterface  */
        private $messageBus;
    
        public function __construct(
            MessageBusInterface $messageBus
        ) {
            $this->messageBus = $messageBus;
        }
    
        /**
         * @param array $criteria
         *
         * @return mixed
         *
         * @throws CriteriaNotAllowedException
         */
        public function getByCriteria(array $criteria)
        {
            $getCustomerQueryList = new GetCustomerListQuery();
    
            foreach ($criteria as $key => $value) {
                $method = 'set' . ucfirst($key);
    
                if (!method_exists($getCustomerQueryList, $method)) {
                    throw new CriteriaNotAllowedException(sprintf('Parameter %s not allowed', $key));
                }
    
                $getCustomerQueryList->$method($value);
            }
    
            return $this->messageBus->dispatch($getCustomerQueryList);
        }
    
        public function get(string $id)
        {
            return $this->messageBus->dispatch(
                new GetCustomerQuery($id)
            );
        }
    
        public function delete(string $id)
        {
            $this->messageBus->dispatch(
                new DeleteCustomerCommand($id)
            );
        }
    }

That’s all, Handlers and Handler interfaces they don’t change.  Below is an example of how they are implemented:

Command:

<?php

namespace App\Domain\Command\Customer;

class DeleteCustomerCommand
{
    /**
     * @var string
     */
    private $id;

    public function __construct(string $id)
    {
        $this->id = $id;
    }

    /**
     * @return string
     */
    public function getId() : string
    {
        return $this->id;
    }
}

Handler Interface:

<?php

namespace App\Domain\CommandHandler\Customer;

use App\Domain\Command\Customer\DeleteCustomerCommand;

interface DeleteCustomerCommandHandlerInterface
{
    public function __invoke(DeleteCustomerCommand $deleteCustomerCommand);
}

Handler:

<?php

namespace App\Infrastructure\CommandHandler\Orm\Customer;

use App\Domain\Command\Customer\DeleteCustomerCommand;
use App\Domain\CommandHandler\Customer\DeleteCustomerCommandHandlerInterface;
use App\Infrastructure\Exception\Customer\CustomerNotFoundException;
use App\Infrastructure\Orm\Repository\CustomerRepository;

class DeleteCustomerCommandHandler implements DeleteCustomerCommandHandlerInterface
{
    /**
     * @var CustomerRepository
     */
    private $customerRepository;

    public function __construct(CustomerRepository $customerRepository)
    {
        $this->customerRepository = $customerRepository;
    }

    /**
     * @param DeleteCustomerCommand $deleteCustomerCommand
     *
     * @throws CustomerNotFoundException
     * @throws \Doctrine\ORM\ORMException
     * @throws \Doctrine\ORM\OptimisticLockException
     */
    public function __invoke(DeleteCustomerCommand $deleteCustomerCommand)
    {
        $customer = $this->customerRepository->findOneBy(['id' => $deleteCustomerCommand->getId()]);

        if (!$customer) {
            throw new CustomerNotFoundException('Customer not found');
        }

        $this->customerRepository->remove($customer);
    }
}

4 thoughts on “CQRS is easy with Symfony 4 and his Messenger Component

  1. Nice article. Thanks for this and the “Symfony + API Platform + CQRS” post. I am getting ready to implement this is my application using Symfony4 and the API Platform and this was very helpful.

    Like

  2. Hey, when posting about symfony and especially 4, it would be nice to use its powers, not just one component.
    resource: ‘../src/Infrastructure/QueryHandler/*’
    exclude: ‘../src/Infrastructure/QueryHandler/{*}.php’ – why are you excluding every php file?

    $method = ‘set’ . ucfirst($key); // you shouldn’t do that. There is PropertyAccessor which will do the job for you.

    Also you don’t need the tags in symfony 4. You should use autoconfigure.

    Liked by 1 person

  3. I found this article because I am in the same situation like you. But I think you didn’t solve this issue well. I have a Domain namescpace too and I can’t just include some Infrastructure-Code in Domain-Logic. I am wonder why you do this, it breaks all rules of DDD.

    Like

Leave a comment