3 Useful services for Symfony applications

A lot of the time you find yourself writing a lot of the same code over and over. Not only is this annoying, but it’s also prone to copy / paste errors. That is why you should stop repeating yourself and leverage service classes in your applications. Today I’ll share some of my services for Symfony which I find very useful in my day to day life as a developer. Feel free to use them in your applications.

Logging service

Logging is essential to see what happened in your application when and error occurred. Because I didn’t want to scatter my application with log statements enclosed in null checks to see if the logger is actually available, I created this LogService.

Continue Reading “3 Useful services for Symfony applications”

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