Symfony + API Platform + CQRS (Part 1)

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

 

References:

12 thoughts on “Symfony + API Platform + CQRS (Part 1)

      1. 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”.

        Like

  1. 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?

    Like

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