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/*'
        exclude: '../src/Infrastructure/QueryHandler/{*}.php'
        public: true
        tags: [messenger.message_handler]
    App\Infrastructure\CommandHandler\:
        resource: '../src/Infrastructure/CommandHandler/*'
        exclude: '../src/Infrastructure/CommandHandler/{*}.php'
        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);
    }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s