Command Query Segregation Responsibility (CQRS)
Introduction
CQRS, stand for Command query responsibility segregation, is a design pattern that involve a complete separation between reading and writing data in your application. The command is responsible of writing data and the query stand for reading. The first thing that come in mind when you read this little definition is:
Why do I need to implement this design pattern? Still, there is already CRUD pattern which stand for creating, reading, updating and deleting data.
As Martin Fowler explain it very well in his tutorial here, CQRS become very useful when your are dealing with a very sophisticated behavior. beside, the CRUD pattern is not enough when we need to store data that's different from the data we provided.
CQRS and Domain Driven Development implementation.
As, I'm passionate of using design pattern as soon as it is possible during my application development process, I join CQRS with Hexagonal Architecture. pattern.
The little example is a template application. it's only goal is to be used for creating, deleting and updating template. The application architecture is like this.
Application
- CreateTemplateController
- UpdateTemplateController
Domain
- Command
- CreateTemplate
- CreateTemplateHandler
- UpdateTemplate
- UpdateTemplateHandler
- Query
- GetTemplateDataProvider
- GetTemplateHandler
- GetTemplateQuery
- Command
Infrastructure(nothing in this folder for this example)
Command part Code
All controllers are implemented following the one action, one controller pattern. This mean that in each controller, there is only one magic method (__invoke), that implement all the logic.
Create Template Controller
<?php
namespace AppBundle\Application;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use AppBundle\Domain\Command\CreateTemplate;
use AppBundle\Domain\DTO\TemplateDTO;
use AppBundle\InputException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class CreateTemplateController extends AbstractController
{
/**
* @ParamConverter("template", converter="fos_rest.request_body")
*/
public function __invoke(string $id, TemplateDTO $template, ?ConstraintViolationListInterface $validationErrors)
{
if ($validationErrors && \count($validationErrors) > 0) {
throw InputException::create(InputException::TEMPLATE_NOT_FOUND, (array) $validationErrors);
}
$template = $this->getCreateTemplate()->execute($template, $id);
return $this->getApiSuccessResponse(['template' => $template]);
}
private function getCreateTemplateCommand(): CreateTemplate
{
return $this->get('app.domain.cqrs.create.template.command');
}
}
Create Template Command
<?php
namespace AppBundle\Domain\Command\Template;
use AppBundle\Domain\Command\CommandInterface;
use AppBundle\Domain\DTO\TemplateDTO;
use AppBundle\Domain\Factory\TemplateDTOFactory;
use AppBundle\Exception\InputException;
class CreateTemplate implements CommandInterface
{
/**@var TemplateDTOFactory*/
private $templateDTOFactory;
/** @var CreateTemplateHandler */
private $createTemplateHandler;
public function __construct(
TemplateDTOFactory $templateDTOFactory,
CreateTemplateHandler $createTemplateHandler
) {
$this->templateDTOFactory = $templateDTOFactory;
$this->createTemplateHandler = $templateDTOFactory;
}
public function execute(object $templateDTO, ?string $objectIdentifier): object
{
if (!$templateDTO instanceof TemplateDTO) {
throw InputException::create(InputException::TEMPLATE_NOT_FOUND, [
\sprintf("Invalid template body parameter")
]);
}
$template = $this->createTemplateHandler->handle($templateDTO);
return $this->templateDTOFactory->create($template);
}
}
CreateTemplateHandler
<?php
namespace AppBundle\Domain\Command\Template;
use Doctrine\ORM\EntityManagerInterface;
use AppBundle\Domain\DTO\TemplateDTO;
use AppBundle\Domain\Factory\TemplateFactory;
use AppBundle\Domain\Query\TemplateType\TemplateTypeDataProvider;
use AppBundle\Entity\Template;
use AppBundle\Entity\TemplateType;
use AppBundle\Exception\InputException;
class CreateTemplateHandler
{
/** @var EntityManagerInterface */
private $entityManager;
/** @var TemplateFactory */
private $templateFactory;
/** @var TemplateTypeDataProvider */
private $templateTypeDataProvider;
public function __construct(
EntityManagerInterface $entityManager,
TemplateFactory $templateFactory,
TemplateTypeDataProvider $templateTypeDataProvider
) {
$this->entityManager = $entityManager;
$this->templateFactory = $templateFactory;
$this->templateTypeDataProvider = $templateTypeDataProvider;
}
public function handle(TemplateDTO $templateDTO): Template
{
try {
/** @var Template $template */
$template = $this->TemplateFactory->create($templateDTO);
$templateTypeIdentifier = $templateDTO->getTemplateType()->getIdentifier();
$templateType = $this->TemplateTypeDataProvider->getItem(
TemplateType::class, $templateTypeIdentifier
);
if(!$templateType) {
throw InputException::create(InputException::_NOT_FOUND, [
\sprintf(
"There is no type for this template corresponding to %s",
$templateTypeIdentifier
)
]);
}
$template->setTemplateType($templateType);
$this->entityManager->persist($template);
$this->entityManager->flush();
return $template;
} catch (\Exception $exception) {
throw InputException::create(InputException::ING_DOCTRINE_ERROR, [$exception->getMessage()]);
}
}
}
Query Part
GetTemplateDataProvider
<?php
namespace AppBundle\Domain\Query\Template;
use AppBundle\Entity\Template;
class TemplateDataProvider
{
private $getTemplateHandler;
public function __construct(GetTemplateHandler $getTemplateHandler)
{
$this->getTemplateHandler = $getTemplateHandler;
}
public function supports(string $resourceClass): bool
{
return Template::class === $resourceClass;
}
public function getItem(string $resourceClass, string $identifier)
{
if (!$this->supports($resourceClass)) {
throw new \Exception("ResourceClassNotSupportedException");
}
$template = $this->getTemplateTypeHandler->handle((new GetTemplateQuery())->setId($identifier));
return $template;
}
}
TemplateHandler
<?php
namespace AppBundle\Domain\Query\Template;
use Doctrine\ORM\EntityManagerInterface;
use AppBundle\Entity\Template;
use AppBundle\Exception\InputException;
class GetTemplateHandler
{
/** @var EntityManagerInterface */
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function handle(GetTemplateQuery $templateQuery): Template
{
$templateRepository = $this->entityManager->getRepository(Template::class);
$template = $templateRepository->findOneBy(['id' => $templateQuery->getId()]);
if(!$template instanceof Template) {
throw InputException::create(
InputException::EMAIL_NOT_FOUND,
[
\sprintf(
"Unable to find template with id %s",
$templateQuery->getId()
)
]
);
}
return $template;
}
}
GetTemplateQuery
<?php
namespace AppBundle\ing\Domain\Query\Template;
class GetTemplateQuery
{
private $id;
public function getId(): string
{
return $this->id;
}
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
}
Et voila :). have Fun