I had to create an API REST for my client. Nothing could be simpler with API Platform and Symfony.
For this project the stack is as follows:
Everything is managed with Docker. Here is an example of my docker-compose
The following example is based on a simple resource called MobileDevice.
The goal is to have a « get » method that returns the desired element and a « post » method to add an element.
Step 1: We create the entity that represents the resource
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class MobileDevice
*
* @ORM\Entity(repositoryClass="AppBundle\Repository\MobileDeviceRepository")
*/
class MobileDevice
{
/**
* @var int
*
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string
* @ORM\Column(type="string")
*/
private $deviceId;
/**
* @var \DateTime
* @ORM\Column(type="datetime")
*/
private $installationDate;
/**
* @var string
* @ORM\Column(type="string")
*/
private $userAgent;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return string
*/
public function getDeviceId(): string
{
return $this->deviceId;
}
/**
* @param string $deviceId
*/
public function setDeviceId(string $deviceId)
{
$this->deviceId = $deviceId;
}
/**
* @return \DateTime
*/
public function getInstallationDate(): \DateTime
{
return $this->installationDate;
}
/**
* @param \DateTime $installationDate
*/
public function setInstallationDate(\DateTime $installationDate)
{
$this->installationDate = $installationDate;
}
/**
* @return string
*/
public function getUserAgent(): string
{
return $this->userAgent;
}
/**
* @param string $userAgent
*/
public function setUserAgent(string $userAgent)
{
$this->userAgent = $userAgent;
}
}
Step 2: We create the DTO (Data Transform Object) which will represent the real resource mapped by API Platform
<?php
namespace AppBundle\Dto;
use ApiPlatform\Core\Annotation\ApiProperty;
use Symfony\Component\Validator\Constraints as Assert;
class MobileDevice
{
/**
* @var string
*
* @ApiProperty(identifier=true)
* @Assert\NotBlank()
*/
private $deviceId;
/**
* @var \DateTime
* @Assert\NotBlank()
*/
private $installationDate;
/**
* @return string
*/
public function getDeviceId(): string
{
return $this->deviceId;
}
/**
* @param string $deviceId
*/
public function setDeviceId(string $deviceId)
{
$this->deviceId = $deviceId;
}
/**
* @return \DateTime
*/
public function getInstallationDate(): \DateTime
{
return $this->installationDate;
}
/**
* @param \DateTime $installationDate
*/
public function setInstallationDate(\DateTime $installationDate)
{
$this->installationDate = $installationDate;
}
}
Step 3: API Platform mapping config file
'AppBundle\Dto\MobileDevice':
itemOperations:
get:
method: 'GET'
path: '/mobile/device/{id}'
collectionOperations:
post:
method: 'POST'
path: '/mobile/device'
At this point we can begin to implement our CQRS pattern.

For those who do not know what CQRS is, we invite you to take a look at these interesting articles:
French language:
- CQRS PATTERN – Eleven Labs
- CQRS ARCHITECTURE PARTIE 1 – Octo Blog
- CQRS ARCHITECTURE PARTIE 2 – Octo Blog
- A la rencontre d’une architecture méconnue : CQRS – Clever Blog
English language:
- CQRS Documents, Greg Young
- CQRS Journey, Dominic Betts, Julián Domínguez, Grigori Melnik, Fernando Simonazzi, Mani Subramanian
- Clarified CQRS, Udi Dahan
- CQRS and Event Sourcing – Code on the Beach 2014, Greg Young
Step 5: Creating Query and QueryHandler objects that are in charge of reading
<?php
namespace AppBundle\Query;
class GetMobileDeviceQuery
{
/** @var string */
private $deviceId;
/**
* MobileDevice constructor.
* @param string $deviceId
*/
public function __construct(string $deviceId)
{
$this->deviceId = $deviceId;
}
/**
* @return string
*/
public function getDeviceId(): string
{
return $this->deviceId;
}
}
<?php
namespace AppBundle\Query;
use AppBundle\Repository\MobileDeviceRepository;
use Doctrine\ORM\EntityNotFoundException;
class GetMobileDeviceQueryHandler
{
/** @var MobileDeviceRepository */
private $mobileDeviceRepository;
/**
* MobileDeviceHandler constructor.
*
* @param MobileDeviceRepository $mobileDeviceRepository
*/
public function __construct(MobileDeviceRepository $mobileDeviceRepository)
{
$this->mobileDeviceRepository = $mobileDeviceRepository;
}
/**
* @param GetMobileDeviceQuery $mobileDeviceQuery
* @return \AppBundle\Entity\MobileDevice|null
* @throws EntityNotFoundException
*/
public function handle(GetMobileDeviceQuery $mobileDeviceQuery)
{
/** @var \AppBundle\Entity\MobileDevice|null $mobileDevice */
$mobileDevice = $this->mobileDeviceRepository->findOneBy(['deviceId' => $mobileDeviceQuery->getDeviceId()]);
if (!$mobileDevice) {
throw new EntityNotFoundException('Device not found');
}
return $mobileDevice;
}
}
Step 6: Creating Command and CommandHandler objects that are in charge of writing
<?php
namespace AppBundle\Command;
class AddMobileDevice
{
/**
* @var string
*/
private $deviceId;
/**
* @var \DateTime
*/
private $installationDate;
/**
* @var string
*/
private $userAgent;
/**
* MobileDevice constructor.
* @param string $deviceId
* @param \DateTime $installationDate
*/
public function __construct(string $deviceId, \DateTime $installationDate, string $userAgent)
{
$this->deviceId = $deviceId;
$this->installationDate = $installationDate;
$this->userAgent = $userAgent;
}
/**
* @return string
*/
public function getDeviceId(): string
{
return $this->deviceId;
}
/**
* @return \DateTime
*/
public function getInstallationDate(): \DateTime
{
return $this->installationDate;
}
/**
* @return string
*/
public function getUserAgent(): string
{
return $this->userAgent;
}
}
<?php
namespace AppBundle\Command;
use AppBundle\Repository\MobileDeviceRepository;
use AppBundle\Entity\MobileDevice;
class AddMobileDeviceHandler
{
/** @var MobileDeviceRepository */
private $mobileDeviceRepository;
/**
* MobileDeviceHandler constructor.
*
* @param MobileDeviceRepository $mobileDeviceRepository
*/
public function __construct(MobileDeviceRepository $mobileDeviceRepository)
{
$this->mobileDeviceRepository = $mobileDeviceRepository;
}
public function handle(AddMobileDevice $command)
{
$deviceMobile = new MobileDevice();
$deviceMobile->setDeviceId($command->getDeviceId());
$deviceMobile->setInstallationDate($command->getInstallationDate());
$deviceMobile->setUserAgent($command->getUserAgent());
$this->mobileDeviceRepository->save($deviceMobile);
}
}
Step 7: Creating Data Provider for retrieve resource
<?php
namespace AppBundle\DataProvider;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
use AppBundle\Dto\MobileDevice;
use AppBundle\Query\GetMobileDeviceQueryHandler;
use AppBundle\Query\GetMobileDeviceQuery;
class MobileDeviceDataProvider implements ItemDataProviderInterface
{
/** @var GetMobileDeviceQueryHandler */
private $getMobileDeviceQueryHandler;
/**
* MobileDeviceWriteSubscriber constructor.
*
* @param GetMobileDeviceQueryHandler $getMobileDeviceQueryHandler
*/
public function __construct(GetMobileDeviceQueryHandler $getMobileDeviceQueryHandler)
{
$this->getMobileDeviceQueryHandler = $getMobileDeviceQueryHandler;
}
/**
* @param string $resourceClass
* @param string|null $operationName
*
* @return bool
*/
public function supports(string $resourceClass, string $operationName = null): bool
{
return MobileDevice::class === $resourceClass;
}
/**
* @param string $resourceClass
* @param int|string $id
* @param string|null $operationName
* @param array $context
* @return MobileDevice
*
* @throws ResourceClassNotSupportedException
*/
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
if (!$this->supports($resourceClass, $operationName)) {
throw new ResourceClassNotSupportedException();
}
/** @var \AppBundle\Entity\MobileDevice $mobileDevice */
$mobileDevice = $this->getMobileDeviceQueryHandler->handle(new GetMobileDeviceQuery($id));
$dtoDeviceMobile = new MobileDevice();
$dtoDeviceMobile->setDeviceId($mobileDevice->getDeviceId());
$dtoDeviceMobile->setInstallationDate($mobileDevice->getInstallationDate());
return $dtoDeviceMobile;
}
}
Step 8: Creating Event Subscriber for add resource
<?php
namespace AppBundle\EventListener\Api;
use AppBundle\Dto\MobileDevice;
use AppBundle\Command\AddMobileDevice as AddMobileDeviceCommand;
use AppBundle\Command\AddMobileDeviceHandler;
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Request;
class MobileDeviceWriteSubscriber implements EventSubscriberInterface
{
/** @var AddMobileDeviceHandler */
private $addMobileDeviceHandler;
/**
* MobileDeviceWriteSubscriber constructor.
*
* @param AddMobileDeviceHandler $addMobileDeviceHandler
*/
public function __construct(AddMobileDeviceHandler $addMobileDeviceHandler)
{
$this->addMobileDeviceHandler = $addMobileDeviceHandler;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
[
'write',
EventPriorities::PRE_WRITE,
],
],
];
}
/**
* @param GetResponseForControllerResultEvent $event
*/
public function write(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
$dtoDeviceMobile = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (!$dtoDeviceMobile instanceof MobileDevice || Request::METHOD_POST !== $method) {
return;
}
$mobileDeviceCommand = new AddMobileDeviceCommand(
$dtoDeviceMobile->getDeviceId(),
$dtoDeviceMobile->getInstallationDate(),
$request->headers->get('User-Agent')
);
$this->addMobileDeviceHandler->handle($mobileDeviceCommand);
}
}
RESULTS POST

RESULTS GET

Next Steps
- Create a CommandBus
- Create a QueryBus
- Create a LocatorHandler
- Separate the read and the write data storage
References:
What are the benefits of a command and handler for the query side (GetMobileDeviceQuery and GetMobileDeviceQueryHandler)? Would a simple repository suffice?
Hi Ian. More infos here: http://cqrs.nu/Faq/command-handlers.
Thanks, I’m familiar with command handlers, I was asking, why would you use it for the read side. It makes sense to me on the write side, but for reading I’m not sure I see the benefit?
Actually you are not obliged to use a QueryHandler, but for a question of uniformity and understanding of the pattern I decided to use it.
Hey, in which file did you place the mapping config?
is there a repository with example ? don’t know where to put the step 3 ^^
Hi, i have not a public repository, sorry. But for the step 3 it’s easy, because it’s the classic configuration of Api Platform:
https://api-platform.com/docs/core/operations
oh ! sorry ^^ i use more often symfony than api-platform, thanks for the answer
Except it’s not: under « api_platform ». Available options are « allo
w_plain_identifiers », « collection », « default_operation_path_resolver », « description », « doctrine », »
doctrine_mongodb_odm », « eager_loading », « elasticsearch », « enable_docs », « enable_entrypoint », « enabl
e_fos_user », « enable_nelmio_api_doc », « enable_profiler », « enable_re_doc », « enable_swagger », « enable
_swagger_ui », « error_formats », « exception_to_status », « formats », « graphql », « http_cache », « mapping »
, « mercure », « messenger », « name_converter », « oauth », « patch_formats », « path_segment_name_generator »
, « resource_class_directories », « show_webby », « swagger », « title », « validator », « version ».
Hi, this is a very helpful article.
I would like to know if there is a reason why you used an Event Subscriber instead of Data Persisters?… or in the other way: why did you use Data Providers instead of Controller Actions?
Hi German, i follow the api-platform documentation: https://api-platform.com/docs/core/data-providers/
[…] Today i want to show you how to use The Messanger Component of Symfony. Very useful when your project implements the CQRS pattern. […]