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:

English language:

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: