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”

Using the Symfony Messenger component

When developing a large scale application the software often needs to do a lot of different tasks. These tasks can consume a lot of time. Because you want your application to be fast and offer a great user experience you don’t want to process these tasks immediately. This is where the Symfony Messenger component comes into play.

What is the Symfony Messenger component?

The Messenger component is an easy to use component for sending and receiving messages to and from a message bus. The bus consists of different middleware classes. This middleware can perform different actions on the messages and manipulate the metadata of the messages. There are a few concepts we need to cover before diving in too deep.

Continue Reading “Using the Symfony Messenger component”

Integrate API’s in React and Redux with ease

When building a frontend for a web application in React and Redux it can be really tedious to integrate the backend API into it. You have to integrate a http library like axios, write the code to do the requests and much more. In your components you’ll call the requests and you have to keep track of the status of the requests. This creates a lot of boilerplate code, which can be really annoying. To combat this the folks at Redux created a nice tool called RTK Query.

What is RTK Query?

RTK Query is part of the Redux Toolkit. It’s a tool specifically created for fetching and caching data. It is built on top of other Redux Toolkit components like createAsyncThunk and createSlice. This is what makes it so puwerful.

The API it exposes is really easy to use and removes a lot of the boilerplate code that was necessary in the past. For example when you want to fetch the data of the current user you had to do it like this.

import axios from 'axios'; export default function UserMenu() { const [user, setUser] = useState(undefined); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); useEffect(() => { if (!user) { setIsLoading(true); axios.get('/api/users/me') .then(response => { setUser(response.data); setIsLoading(false); }) .catch(error => { setError(error); setIsLoading(false); }); } }); return ( <div className="menu"> {isLoading && <Loader />} {!isLoading && !error && <span className="username">{user.name}</span>} </div> ) }
Code language: JavaScript (javascript)

As you can see in the example you’ll have to manage the loading and error states by yourself. With RTK Query you don’t have to do this. Let me explain that with the same example as above, but this time with RTK Query.

import {useGetMeQuery} from './redux/api'; export default function UserMenu() { const {data: user, isLoading, error} = useGetMeQuery(); return ( <div className="menu"> {isLoading && <Loader />} {!isLoading && !error && <span className="username">{user.name}</span>} </div> ) }
Code language: JavaScript (javascript)

Now doesn’t this look a lot cleaner? You get the same functionality as the example above, but it much cleaner. And it gets even better.

Setup RTK Toolkit

It comes installed with the Redux Toolkit library. To install this toolkit simple install @reduxjs/toolkit using your favorite package manager like yarn or npm. I’m going to assume you’ve setup your Redux Store already, so I’ll skip this part. Now we can implement our API using the createApi function provided by the RTK Query tool. Let’s create our user API:

import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react' const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.org/', prepareHeaders: (headers, {getState}) => { const token = getState().auth.token; if (token) { headers.set('Authorization', `Bearer ${token}`) } return headers; } }), endpoints: build => ({ getMe: build.query({ query: () => (`/api/users/me`), }), createUser: build.mutation({ query: (request) => ({ url: '/api/users', method: 'POST', body: request, }), }), }), }); export const {useGetMeQuery, useCreateUserMutation} = api; export default api;
Code language: JavaScript (javascript)

It doesn’t take much to define an API in RTK Query. We have to define the baseUrl and for security we’ve added an extra header using prepareHeaders which contains the bearer token. This callback has access to the state of your Redux store, so we can fetch the JWT Token from it.

The next part is the definition of the endpoints. We’ve added support for the /api/users/me endpoint which returns the user details of the owner of the JWT Token and a createUser endpoint, which, like the name implies creates a new user.

Then we export the react hooks created by the createApi function, which we can use in our application. And lastly we export the api object.

To make this all work we need to register our API to the Redux Store, so let’s make this happen!

import {configureStore} from '@reduxjs/toolkit'; import api from './api'; import authReducer from './slices/auth'; // Define all reducers const reducer = { [api.reducerPath]: api.reducer, auth: authReducer, }; export const store = configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), devTools: process.env.NODE_ENV !== 'production', });
Code language: JavaScript (javascript)

First we need to add the reducer created by the createApi function. This will make sure the data from the API calls will be stored in the Redux store. The next change we’ve made is to add the middleware the createApi function created to the store using middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware). This will make sure the advanced features of RTK Query like caching and such works correctly.

Code splitting

When you have an API with a lot of endpoints, your api.js file will become pretty large and a nightmare to maintain. To remedy this RTK Query made it possible to inject endpoints in an existing API object. This makes the files a lot smaller and easier to maintain.

Let start with the base api.ts file into which we can inject the extra endpoints from the other files.

import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react' // Initialize an empty api service that we'll inject endpoints into later as needed const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.org', prepareHeaders: (headers, { getState }) => { const token = getState().auth.token; if (token) { headers.set('Authorization', `Bearer ${token}`) } return headers; } }), endpoints: () => ({}), }); export default api;
Code language: JavaScript (javascript)

It looks pretty much the same as the other example, but we initialized the endpoints with an empty object. We’ll inject the endpoints later in the different files we’ll create.

import api from "./api"; const usersApi = api.injectEndpoints({ endpoints: build => ({ getMe: build.query({ query: () => (`/api/users/me`), }), createUser: build.mutation({ query: (request) => ({ url: '/api/users', method: 'POST', body: request, }), }), }), }); export const { useGetMeQuery, useCreateUserMutation } = usersApi; export default usersApi;
Code language: JavaScript (javascript)

The object returned from the createApi function has an injectEndpoints method which we’ll use in our ‘API Extension points’. In the injectEndpoints method we can simply add the endpoints we want.

Because we’ve already added the base API to our Redux store, there is no need to add extra reducers and middleware for our injected endpoints. When we want to load the current user we can simply use the useGetMeQuery from the usersApi.ts file above. This works the same as before.

Extra options

I haven’t even touched the surface of all the capabilities of the RTK Query tool. There are many more options for caching the data, refetching when needed and many more. I’ll write another article about this in the future. For now check out their official documentation for the information.

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/workflow
Code 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: cancelled
Code 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?