How to use custom DTO’s in API Platform?

Normally in API Platform all requests are linked to an entity or another model class. API Platform then creates the basic CRUD (create, read, update, delete) endpoints, such as GET, POST, PUT and DELETE. This is fine for most basic applications, but most of the time you need custom endpoints with custom data. This is where DTO’s (Data Transfer Objects) come in handy. In this guide I’ll show you how to use custom DTO’s in API Platform.

What are we going to build?

To explain the concepts more easily I’m going to create a custom endpoint for user registration in our REST API. We need a custom endpoint because we want to restrict the POST method on the /api/users endpoint to admins only with some special features. The endpoint we’ll create will be a POST on /api/users/register which will be used in our frontend application for user registration.

To make sense of it all I’ll show you my user entity:

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;

use App\Repository\UserRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ApiResource(
    collectionOperations: [
        'get' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],        
        'post' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],
 
    ],
    itemOperations: [
        'get' => [
            'security' => 'object == user or is_granted("ROLE_ADMIN")',
        ],
        'put' => [
            'security' => 'object == user or is_granted("ROLE_ADMIN")',
        ],
        'delete' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],
    ],
    denormalizationContext: ['groups' => ['user:write']],
    normalizationContext: ['groups' => ['user:read']],
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    const ROLE_ADMIN = 'ROLE_ADMIN';
    const ROLE_USER = 'ROLE_USER';

    #[ORM\Id()]
    #[ORM\Column(type: 'guid', unique: true)]
    #[ORM\GeneratedValue(strategy: 'UUID')]
    #[Groups(['user:read'])]
    private string $id;

    #[ORM\Column(type: "string", length: 1024, nullable: false)]
    #[Groups(['user:read', 'user:write'])]
    private string $email;

    #[ORM\Column(type: "string", length: 255, nullable: false)]
    #[Groups(['user:write'])]
    private string $password;

    #[ORM\Column(type: "string", length: 255, nullable: false)]
    #[Groups(['user:read', 'user:write'])]
    private string $firstName;

    #[ORM\Column(type: "string", length: 255, nullable: false)]
    #[Groups(['user:read', 'user:write'])]
    private string $lastName;

    #[ORM\Column(type: "string", length: 20, nullable: false)
    #[Groups(['user:read', 'user:write'])]
    private string $role;

    #[ORM\Column(type: "boolean")]
    #[Groups(['user:read', 'user:write'])]
    private bool $active;

    // Getters and setters removed for brevity
}Code language: PHP (php)

As you can see it’s a fairly basic user entity with fields for the credentials and some userdata. All of the endpoints are accessible for admins only, with the exception of GET /api/users/{id} and PUT /api/users/{id} which allows users to change their own profile and read their own profile. The properties returned are managed by the serialization groups user:read and user:write.

What is a DTO?

First I’ll explain the term DTO. It’s an acronym for Data Transfer Object. It’s a simple object used to transfer data from one place to another. Let’s start with an example.

namespace App\Dto\Request\User;

use Symfony\Component\Validator\Constraints as Assert;

/**
 * Register User Request DTO
 */
class RegisterUserRequest
{
    public string $firstName;
    public string $lastName;
    public string $email;
    public string $password;
}Code language: PHP (php)

As you can see this is a very simple PHP class with public properties. You can also make the properties private and use getters and setters to access the values.

Creating the custom endpoint

In order to create the custom endpoint we first need to create a controller class. Let’s call it Register.

namespace App\Controller\User;

use App\Application\Service\UserService;
use App\Dto\Request\User\RegisterUserRequest;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * Handles the registration of a new user
 */
class Register extends AbstractController
{
    /**
     * @param UserService $userService
     * @param SerializerInterface $serializer
     */
    public function __construct(private UserService $userService, private SerializerInterface $serializer) {}

    /**
     * @param Request $request
     *
     * @return User
     */
    public function __invoke(Request $request): User
    {
        $dto = $this->serializer->deserialize($request->getContent(), RegisterUserRequest::class, 'json');
        
        return $this->userService->register($dto);
    }
}Code language: PHP (php)

We use the Symfony serializer to convert the request data to our custom DTO. This DTO is sent to the userService which handles the actual creation of an account for the user.

Next we need to make sure API Platform knows about this controller and when to use it. To do this we can register this controller in our user entity.

namespace App\Entity;

// Uses removed for brevity

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ApiResource(
    collectionOperations: [
        'get' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],        
        'post' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],
        'register' => [
            'method' => 'POST',
            'status' => 204,
            'path' => '/users/register',
            'controller' => Register::class,
            'read' => false,
            'deserialize' => false,
        ],
 
    ],
    itemOperations: [
        // Removed for brevity
    ],
    denormalizationContext: ['groups' => ['user:write']],
    normalizationContext: ['groups' => ['user:read']],
)]
#[UniqueEntity(fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    // Removed for brevity
}Code language: PHP (php)

As you can see I’ve added a register entry to the collectionOperations property of the ApiResource attribute. This will create an extra endpoint which we can reach on /api/users/register. I’ve set the status to 204 because we will return a 204 No Content response when user registration was successful. When something went wrong we’ll return a 4xx or a 5xx http code to indicate problems with registration.

The read and deserialize options are set to false, because we don’t want API Platform to try and deserialize requests to this endpoint. We’re handling this ourselves in the controller.

Using events to automate this

You can imagine when you have a lot of custom requests it can be really tedious to inject the serializer and to the deserialization manually all the time. Of course we can change this, we are developers after all. We can leverage the events emitted by API Platform. In our case we’ll create a subscriber that listens to KernelEvents::REQUEST and EventPriorities::PRE_READ this will trigger our event on every request, before data is read by API Platform.

namespace App\Event\Subscriber;

use ApiPlatform\Core\EventListener\EventPriorities;
use ApiPlatform\Core\Validator\ValidatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * Serializes the request data to the given DTO
 */
final class DtoSerializerEventSubscriber implements EventSubscriberInterface
{
    /**
     * @param SerializerInterface $serializer
     * @param ValidatorInterface $validator
     */
    public function __construct(private SerializerInterface $serializer, private ValidatorInterface $validator)
    {}

    /**
     * @param RequestEvent $event
     */
    public function serializeToDto(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if (false === $request->attributes->has('dto')
            || false === in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH], true)
        ) {
            return;
        }

        $dto = $this->serializer->deserialize($request->getContent(), $request->attributes->get('dto'), 'json');
        $this->validator->validate($dto);

        $request->attributes->set('dto', $dto);
    }

    /**
     * @return array[]
     */
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['serializeToDto', EventPriorities::PRE_READ],
        ];
    }
}Code language: PHP (php)

In this subscriber we check if it’s a request that modifies data (POST, PUT, PATCH), because we can expect custom DTO’s in these request methods. We also check if the request has a dto attribute, if one of these checks fail, we simple exit the event subscriber by returning.

When all checks passed we deserialize the request data using the classname in the dto attribute. As an added bonus we also validate the DTO using attributes from the Validator component of Symfony. This makes sure our DTO is always valid, and if it’s not the API Platform will return the validation failures for us. Very nice!

After the validation is done we simple put the deserialized DTO in the request using $request->attributes->set('dto', $dto);

We still have one loose end to fix. How do we get the classname of the DTO we want to use in the request attributes? Simple! We can add it to the declaration of our custom endpoint like this.

namespace App\Entity;

// Uses removed for brevity

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ApiResource(
    collectionOperations: [
        'get' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],        
        'post' => [
            'security' => 'is_granted("ROLE_ADMIN")',
        ],
        'register' => [
            'method' => 'POST',
            'status' => 204,
            'path' => '/users/register',
            'controller' => Register::class,
            'defaults' => [
                'dto' => RegisterUserRequest::class,
            ],
            'read' => false,
            'deserialize' => false,
        ],
 
    ],
    itemOperations: [
        // Removed for brevity
    ],
    denormalizationContext: ['groups' => ['user:write']],
    normalizationContext: ['groups' => ['user:read']],
)]
#[UniqueEntity(fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    // Removed for brevity
}Code language: PHP (php)

We’ve added defaults to the declaration of our register endpoint in which we specify the DTO to use.

Using this event subscriber will make our future custom controllers easier to use because we can simply get the serialized DTO from the request attributes. To do this use $request->attributes->get('dto'). So our custom controller now looks like this.

namespace App\Controller\User;

use App\Application\Service\UserService;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;

/**
 * Handles the registration of a new user
 */
class Register extends AbstractController
{
    /**
     * @param UserService $userService
     */
    public function __construct(private UserService $userService) {}

    /**
     * @param Request $request
     *
     * @return User
     */
    public function __invoke(Request $request): User
    {
        return $this->userService->register($request->attributes->get('dto'));
    }
}Code language: PHP (php)

Very nice and clean code, just the way I like it!

Validation of custom DTO’s

As I’ve mentioned in the previous section I’ve added the validator to the event subscriber to validate the DTO’s. To make full use of this we need to alter our request DTO a tiny bit. The validator component needs to know how to validate every property of the DTO. We need to add special attributes to the property in order to do this.

namespace App\Dto\Request\User;

use Symfony\Component\Validator\Constraints as Assert;

/**
 * Register User Request DTO
 */
class RegisterUserRequest
{
    #[Assert\NotBlank]
    #[Assert\Length(max: 255)]
    public string $firstName;

    #[Assert\NotBlank]
    #[Assert\Length(max: 255)]
    public string $lastName;

    #[Assert\NotBlank]
    #[Assert\Email]
    #[Assert\Length(max: 1024)]
    public string $email;

    #[Assert\NotBlank]
    #[Assert\Length(min: 8)]
    public string $password;
}Code language: PHP (php)

These Assert\* attributes will tell the validator how to validate the properties of our DTO.

The use of different Symfony components makes it very easy to create custom endpoints for our API with custom DTO’s.

What do you think of this way of using custom DTO’s in API Platform?

The Symfony Workflow component explained

The Symfony workflow component is a very powerful component. Especially when you need to make sure your entities maintain a valid state. This can be very important when, for example, you build an ecommerce system with order handling. In this post we will use the processing of an order as an example.

Setting up the Symfony Workflow Component

To use the workflow component in your application you first need to add it using composer.

composer require symfony/workflowCode language: JavaScript (javascript)

When this command is done executing you can configure the component using YAML.

# config/packages/workflow.yaml
framework:
    workflows:
        order:
            type: 'state_machine'
            audit_trail:
                enabled: true
            marking_store:
                type: 'method'
                property: 'status'
            supports:
                - App\Entity\Order
            initial_marking: pending
            places:
                - pending
                - paid
                - packed
                - shipped
                - cancelled
            transitions:
                pay:
                    from: pending
                    to:   paid
                pack:
                    from: paid
                    to:   packed
                ship:
                    from: packed
                    to:   shipped
                cancel:
                    from: [pending, paid, packed]
                    to:   cancelledCode language: PHP (php)

In this example we’ve defined a workflow for our order. We’ve used a state_machine because our orders can only have one state at a time. Because we’ve enabled the audit_trail we’ll get detailed logs about the transitions, which can be very useful when debugging the workflow.

The next thing we configure is the supported entity (App\Entity\Order) and the property in the entity to store the current state in (status). We set the default status of our orders to pending using the initial_marking option. Next we set the possible states (places) and the transitions we can do using this workflow.

Of course we need to create the order entity, so here it is, in a very basic form.

namespace App\Entity;

class Order 
{
    private array $products;

    // This property will be managed by the workflow component
    private string $status;

    // These getters and setters need to exist for the workflow component
    public function getStatus(): string
    {
        return $this->status;
    }

    public function setStatus(string $status, $context = []): void
    {
        $this->status= $status;
    }
}Code language: PHP (php)

Using the symfony workflow component

Now that the workflow component is set up we can use it in our application. To make this easier for you, the developer, symfony creates a service for you named workflow.[workflowname] in our case this is workflow.order. You can inject this service, or you can use autowiring to access this service. Just like with the caching component in symfony you can also inject the service using a special parameter name. In this case we can use WorkflowInterface $orderWorkflow to inject the workflow.

Lets create a service to handle the orders for us.

namespace App\Service;

use App\Entity\Order;

use App\Exception\OrderStateException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Workflow\Exception\LogicException;

class OrderService
{
    private EntityManagerInterface $entityManager;
    private WorkflowInterface $orderWorkflow;

    public function __construct(EntityManagerInterface $entityManager, WorkflowInterface $orderWorkflow)
    {
        $this->entityManager = $entityManager;
        $this->orderWorkflow = $orderWorkflow;
    }

    public function pay(Order $order): void
    {
        $this->doTransition('pay', $order);
        // Do extra stuff on the order entity here, like setting payment data
        $this->entityManager->flush();
    }

    public function pack(Order $order): void
    {
        $this->doTransition('pack', $order);
        // Do extra stuff on the order entity here, like setting the packed item amount
        $this->entityManager->flush();
    }

    public function ship(Order $order): void
    {
        $this->doTransition('ship', $order);
        // Do extra stuff on the order entity here, like setting the track and trace code
        $this->entityManager->flush();
    }
    
    public function cancel(Order $order): void
    {
        $this->doTransition('cancel', $order);
        $this->entityManager->flush();
    }

    private function doTransition(string $transition, Order $order): void
    {
        try {
            $this->orderWorkflow->apply($order, $transition);
        } catch (LogicException $e) {
            // Throw a custom exception here and handle this in your controller,
            // to show an error message to the user
            throw new OrderStateException(sprintf('Cannot change the state of the order, because %s', $e->getMessage()), 0, $e);
        }
    }
}Code language: PHP (php)

As you can see we’ve created a function for every transition possible, this makes it very easy for us to call these in our controllers. Because every transition change is basically the same, check if it’s possible and then apply it, we’ve created a private helper method doTransition. This method will throw custom exceptions which we can catch in our controller to show helpful messages to our users.

Because we make these methods so lean we can leverage the transactions of the doctrine entity manager. A transition is not stored in the database automatically by the workflow component. You will have to do this manually. Using this makes sure we can change other properties of our entity in the same database transaction. So if it fails it will never end up in a corrupted state. Like having status paid without having payment data stored.

Using the workflow component guarantees the entity is processed in the correct order. An unpaid order will not be able to transition to a packed order. We did this with a little bit of configuration and a few lines of code and of course the Symfony Workflow Component.

Using events to extend your workflow

Let’s go a step further updating our store’s total sales statistics. We could write that functionality in the ship method of the service, but that will add an extra dependency. To circumvent this we can leverage the power of the event dispatcher of symfony.

To be able to do this the workflow component sends out a lot of events when transitioning from one state to another:
workflow.guard – Checking if the state can be changed
workflow.leave – The entity leaves a state
workflow.transition – An entity is going through a transition
workflow.enter – The entity enters a new state
workflow.entered – An entity entered a new state
workflow.completed – The transition is complete

You can make these events more specific by appending the workflow name and the transition name like this:
workflow.order.transition.pay
workflow.order.completed.ship

Let’s create our event subscriber to update the statistics.

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;

class UpdateOrderStatisticsSubscriber implements EventSubscriberInterface
{
    // ... add external dependencies here ...
    public function onShippingComplete(Event $event)
    {
        $order = $event->getSubject();

        // ... perform calculations here ...
    }

    public static function getSubscribedEvents()
    {
        return [
            'workflow.order.completed.ship' => 'onShippingComplete',
        ];
    }
}Code language: PHP (php)

As you can see, it is really easy to extend the actions performed on an transition using the event system.

Using the guard events

The guard events are a special kind of events in the workflow component. These will be dispatched when the workflow component checks to see if you can execute a transition. With the guard events you can block the transition using external factors.

Lets create a guard event that makes sure only users with the role ROLE_PACKER can transition an order to packed.

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Event\Event;

class OnlyPackersCanPackAnOrderSubscriber implements EventSubscriberInterface
{
    private Security $security;

    public function __construct(Security $security) 
    {
        $this->security = $security;
    }

    public function onGuard(Event $event)
    {
        $user = $this->security->getUser();
        
        if (null === $user || false === $this->security->isGranted('ROLE_PACKER')) {
            $event->setBlocked(true, 'Only warehouse employees can pack orders.');
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            'workflow.order.guard.pack' => 'onGuard',
        ];
    }
}
Code language: PHP (php)

This event listener will make sure only warehouse employees (user with the role ROLE_PACKER) can pack orders. Everyone else will get the error message.

Conclusion

The workflow component is essential when you need to maintain a certain workflow within your application. It makes it really easy to maintain the status of an entity. It’s also really easy to adapt or extend the workflow by changing the configuration.

Have you used the workflow component already in an application? What did you use it for?

How to use Extensions in API Platform properly?

Imagine you’re working on this big application which involves an API and a React frontend. You decide on using API Platform for your REST API because of its ease of use. Because the users security is a great concern you want to find an easy way to make sure a user can only access their own data. Ofcourse you can create custom actions where you manually filter the returned data from the database. But this can be really tedious if you have a lot of database entities. To combat this, we can leverage extensions in API Platform.

Extensions in API Platform

Introducing the extensions of API Platform. These classes are called for every request that comes in. These extension classes are able to change the DQL generated by the platform. To create an extension you need to create a class implementing ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface and / or ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface.

If you create a class implemting one or both of these interfaces they will automatically be picked up by the symfony dependency injection container. If you don’t use auto wiring you will need to tag the classes as follows:

services:    
    App\Infrastructure\Api\Extension\ExampleExtension:
        tags:
            - { name: api_platform.doctrine.orm.query_extension.collection }
            - { name: api_platform.doctrine.orm.query_extension.item }Code language: CSS (css)

User filtering using API Platform’s Extensions

In our large application we want to make sure users can only access their own data. To do this efficiently we can harness the power of extensions. Let’s say for example we have a Profile entity and an Address entity which are linked to a user. Both of these entities have a property called user which contains the user it belongs to. To make sure we don’t fetch the data of the wrong user we can use an extension:

namespace App\Infrastructure\Api\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Address;
use App\Entity\Profile;
use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;

/**
 * This extension makes sure normal users can only access their own Addresses and PRofiles
 */
final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    /**
     * @param Security $security
     */
    public function __construct(private Security $security) {}

    /**
     * @param QueryBuilder $queryBuilder
     * @param QueryNameGeneratorInterface $queryNameGenerator
     * @param string $resourceClass
     * @param string|null $operationName
     */
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
    {
        $this->addWhere($queryBuilder, $resourceClass);
    }

    /**
     * @param QueryBuilder $queryBuilder
     * @param QueryNameGeneratorInterface $queryNameGenerator
     * @param string $resourceClass
     * @param array $identifiers
     * @param string|null $operationName
     * @param array $context
     */
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
    {
        $this->addWhere($queryBuilder, $resourceClass);
    }

    /**
     * @param QueryBuilder $queryBuilder
     * @param string $resourceClass
     *
     */
    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
    {
        if (Address::class !== $resourceClass
            || Profile::class !== $resourceClass
            || $this->security->isGranted(User::ROLE_ADMIN)
            || null === $user = $this->security->getUser()
        ) {
            return;
        }

        $rootAlias = $queryBuilder->getRootAliases()[0];
        $queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));

        /** @var User $user */
        $queryBuilder->setParameter('current_user', $user->getId());
    }
}Code language: PHP (php)

We inject the Symfony\Component\Security\Core\Security class so we are able to get the current user of the request. We need to implement the applyToCollection and the applyToItem functions for the two extension interfaces. These methods are called by the API Platform when we do a GET collection request or a GET item request respectively. Because we want to perform the same action in both cases we created the method addWhere which receives the QueryBuilder and the resource class.

Because we only want to change the query for certain cases we check if the resourceClass is supported by the extension or if the user has the role ROLE_ADMIN. If the resourceClass is not supported or the user is an admin we simple exit the function, since we don’t need to add the filter. If it is a supported class and the user is not an admin we use the query builder to add a where clause making sure we only fetch data for the current user.

Protip

If you find yourself adding alot of resourceClass checks to your if statement, try using an interface. You can make an interface called UserDataInterface and change the if statement to check if the resourceClass is an implementation of this interface. For example:

namespace App\Domain\Interface;

use App\Entity\User;

interface UserDataInterface 
{
    public function getUser(): User;
    // ... More methods if needed
}Code language: PHP (php)

You can now change your if-statement to the following:

// ... removed for brevity
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
    if (is_subclass_of($resourceClass, UserDataInterface::class)
        || $this->security->isGranted(User::ROLE_ADMIN)
        || null === $user = $this->security->getUser()
    ) {
        return;
    }

    // ... filter code
}Code language: PHP (php)

It’s a small change, with a large impact. The function is_subclass_of checks if the class (first parameter can be an instance or a string) is a subclass of another class, or interface (the second parameter). So now this extension will add the filter for every class we mark with implements UserDataInterface.

Do you have any useful tips or tricks using API Platform? Leave them in the comments!

How to use Caching in Symfony to speed up your application?

We all need to call external API’s sometimes to fetch some data or do some heavy calculations. Sometimes those external API’s can be very slow which degrades the user experience of our application. To combat this, we can use a caching strategy to make sure we only call the API’s when we really need to. In this tutorial I’ll explain to you how to leverage caching in Symfony to speed up your application.

What is caching?

You guys probably already know what caching is, but for completeness sake I’ll add this paragraph for the people that don’t know already. Caching is a method of (temporarily) storing data in a fast accessible location. This location can be the servers RAM or the filesystem.

When your application wants to fetch some data it first checks the cache. If the data is found there, it’s used. When the data is not in the cache, it is fetched from, for example an external API, and then stored in the cache and used. The next request for the same data then uses the faster cache instead of calling the ‘slow’ external API.

The Symfony framework by itself already uses cache. You’re probably familiar with the bin/console cache:clear command or the var/cache folder. This is because Symfony is a smart framework which caches alot out of the box to ensure your application runs smoothly.

Setup caching in Symfony 5

To setup your own application caching in Symfony you first need to understand a few concepts:

Pools: A pool is a cache service your application will interact with. Each pool is an indepent cache, meaning you can have multiple pools with different settings, but with the same keys for your cache items. This is possible because of namespacing.

Adapters: A cache pool uses one adapter. An adapter is basically the interface to the storage method. For example the filesystem adapter stores the cache on, you guessed it, the file system. Using a special adapter, the so called ChainAdapter, you can add multiple adapters to one cache pool.

Now we got that out of the way, lets setup our basic caching example. In our example we’ll store our cache on the file system.

framework:
    cache:
        # Set the directory we want our cache to be stored in
        directory: '%kernel.project_dir%/cache'

        # Next we create our cache pool and set it to store on the file system
        pools:
            example.cache:
                adapter: cache.adapter.filesystem
                tags: true  # Make sure our pool supports taggingCode language: PHP (php)

In this configuration we have configured a cache pool named example.cache which we can use throughout our application using dependency injection. This pool stores it’s cache in the projectdir/cache folder and supports tagging of cache items.

How to use caching in Symfony?

Now that our caching pool is configured we can use its power. Let’s create a simple service that calls an external API to fetch some data and return it.

<?php

namespace App\Services;

use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

class ExampleService
{
    private TagAwareCacheInterface $exampleCache;

    public function __construct(TagAwareCacheInterface $exampleCache)
    {
        $this->exampleCache = $exampleCache;
    }

    public function getDataFromSlowApi(): array
    {
        return $this->exampleCache->get('cache_key', function (ItemInterface $item) {

            // ...Execute your API Request or caculation here

            $result = 'Data from a very slow API';

            // This cache is valid for 1800 seconds, or 30 minutes
            $item->expiresAfter(1800);

            $item->tag(['blogitems', 'user-1']);

            // Return your result
            return $result;
        });
    }
}Code language: HTML, XML (xml)

Now that we’ve created our ExampleService class lets see how it works. Lets start with the constructor. We inject our cache pool using TagAwareCacheInterface $exampleCache. We are using this name for our parameter because the name of the pool is example.cache. The dependency injection container automatically injects this pool when we name our variable $exampleCache, the camelcased version of example.cache. Very nice!

Next we use this cache pool to make our API call faster. To do this, our cache pool service has a method called get which requires two parameters. A cache key and a callback function. First this method checks that our cache key exists in the cache and it is not expired. If that’s the case, it just returns the value in the cache. When the cache key does not exist in the cache or it is expired, the callback function is called. In this function we can configure the cache item, like the lifetime and we can call the API or do our calculation in here. When we return the result from this function, the result is stored in the cache and returned to our getDataFromSlowApi method.

Ofcourse you can make the cache key what ever you want. You can even make it dynamic based on the id of the logged in user for example. Using this method you can have multiple users have different values in the cache for the same API call. That’s up to you though.

This is basically how to use caching in Symfony, as you can see it’s very easy and straight forward to setup.

BONUS: Tagging our cache items

If you looked closely at the service example above you might have spotted this line of code $item->tag(['blogitems', 'user-1']);. You might be wondering what does it do and why do I need it? Well let me explain the purpose of tagging.

Tagging is very useful when you have a lot of users in your application for whom you need to cache different types of information. In our example the slow API returned blogitems, so we added a tag blogitems to the cache item. The user that was requesting the list was the user with id 1, so we added a tag user-1.

The power of tagging is that when we want to remove all blogitems from our cache, because we switched API or something. We can simply remove all cached items which are tagged with blogitem. Or when user 1 deletes his or her account we can simply remove all cache items which are tagged with user-1. Let me show you how to do that with an example.

<?php

namespace App\Services;

use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

class ExampleService
{
    // ... Code from the previous example here ...

    public function removeUserCache(string $userId): void
    {
        $this->exampleCache->invalidateTags(['user-'.$userId]);
    }

    public function removeBlogItemCache(): void
    {
        $this->exampleCache->invalidateTags(['blogitems']);
    }
}Code language: HTML, XML (xml)

As you can see it’s really easy to manage your cache using tags. Simply call the invalidateTags method of your cache pool with the tag(s) you want to invalidate. If you just want to invalidate the blog items from user 1 simple combine the tags into one invalidateTags call.

What do you think of the caching mechanism in Symfony? Let me know in the comments!

How to inject multiple instances of an interface in a service using Symfony 5?

In this post I will teach you how to inject multiple concrete implementations of an interface into a service. This can be useful when, for example, you need to call different API’s depending on a variable. Your code will be much cleaner when using this method instead of using a lot of if statements.

Define your interface

First we need to define the interface all of our concrete classes will be implementing:

<?php

namespace App\Interfaces;

interface ProviderInterface
{
    /**
     * Function to check wether this implementation supports the $type
     */
    public function supports(string $type): bool;

    /**
     * This function contains the actual logic
     */
    public function execute(ProviderDto $data): ProviderResultDto;
}Code language: HTML, XML (xml)

This will be the interface for all our Provider classes. The supports function will make sure the correct implementation will be called when needed. We can put all of our logic in the execute function.

Creating our providers

Now I’ll create two simple providers implementing this interface.

<?php

namespace App\Providers;

use App\Interfaces\ProviderInterface;

class FirstProvider implements ProviderInterface
{
    public function supports(string $type): bool
    {
        return 'first' === $type; 
    }

    public function execute(ProviderDto $data): ProviderResultDto
    {
        // Some logic here to return the ProviderResultDto
    }
}Code language: HTML, XML (xml)

And the second provider.

<?php

namespace App\Providers;

use App\Interfaces\ProviderInterface;

class SecondProvider implements ProviderInterface
{
    public function supports(string $type): bool
    {
        return 'second' === $type; 
    }

    public function execute(ProviderDto $data): ProviderResultDto
    {
        // Some logic here to return the ProviderResultDto
    }
}Code language: HTML, XML (xml)

These are very simple implementations of the ProviderInterface. Ofcourse you can do what ever you need in these classes.

Create the service

We need a central point to inject these implementations into. We’ll use a service for this. This is a simple service that will act as a factory. We can call the execute function of this service with a few parameters and this service will make sure the correct Povider implementation will be called.

<?php

namespace App\Services;

use App\Interfaces\ProviderInterface;

class ProviderService
{
    /**
     * @var ProviderInterface[]
     */
    private array $providers;

    public function __construct(ProviderInterface ...$providers)
    {
        $this->providers = $providers;
    }

    public function execute(string $type, ProviderDto $data): ?ProviderResultDto
    {
        foreach($this->providers as $provider) {
            if ($provider->supports($type)) {
                return $provider->execute($data);
            }
        }

        return null; // Or throw an ProviderNotSupportedException or something like that
    }
}Code language: HTML, XML (xml)

As you can see this is a very simple service. The constructor will accept an array of providers and stores this in a local parameter. When we want to execute one of the providers we can simple inject this service where you need it and call it’s execute function. This function will then call the correct provider and return the result.

Autowiring our ProviderService

We just need to do one final thing. That is making sure the providers are autowired in the ProviderService. We can use symfony service tags for this. First we need to make sure our implementations of the ProviderInterface are tagged correctly. We can do this by adding the following code to our service.yml file.

# config/services.yaml
services:
    _instanceof:
        App\Interfaces\ProviderInterface:
            tags: ['app.provider']
Code language: PHP (php)

This will make sure all of our providers created now and in the future will have the app.provider tag applied to it. Now that the providers are tagged correctly, we can inject them into our service.

# config/services.yaml
services:
    App\Services\ProviderService:
        arguments:
            $providers: !tagged_iterator app.providerCode language: PHP (php)

We are done! Our service will receive all of our providers and can access them accordingly. When we need to execute a provider we can simple inject our ProviderService and call the execute method.