Use State Processors and Providers on API Platform 3

Etearner
5 min readFeb 19, 2024

--

Hello everyone! It’s me again, the eternal learner.

First of all, I hope this new year began wonderfully on your side and I wish you guys to have a successful coding year.

Today, I’ve decided to delve into the use of state processors and providers in API Platform, a pattern that can significantly enhance your project’s data handling capabilities.

After spending considerable time with the official documentation to understand the mechanism behind state processors and providers, I realized it’s not the simplest of tasks. This article aims to simplify it for anyone looking to leverage these powerful features in their projects.

TL;DR.

What are state processors and providers?

State processors and providers are classes that interact with instances marked as API resources (usually using the #[ApiResource] attribute). Their goal is to manipulate these instances before they are either saved to a datastore or returned in a response.

The key difference between the two is their use cases:

  • State Processors are utilized during operations like POST, PUT, PATCH and DELETE to apply changes.
  • State Providers are employed for GET operations to fetch and possibly transform data before it’s displayed.

Introduced in API Platform 3, these classes replace the previous data persistors and providers, offering a more streamlined and flexible approach to data handling.

How to use state processors and providers?

Let’s explore how to use these patterns through a practical example: managing user entities in a platform.

Consider an API resource to manage platform users, represented by the following entity:

<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\Repository\UserRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'app_user')]
#[ApiResource]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\Column(length: 150, unique: true)]
private string $email;

#[ORM\Column(length: 255)]
private string $password;

private string $plainPassword;

}

To create a User resource, the client sends a request with a plaintext password. For example:

{
"email": "eternallearner@example.com",
"password": "this.Password.Is.Not.Safe"
}

Clearly, storing passwords in plaintext is a security risk, necessitating hashing before persistence. This is where a dedicated state processor comes into play.

Creating a Custom State Processor

To handle user passwords securely, we will implement a custom state processor for hashing.

There are two ways to create a custom state processor:

  1. Use the Symfony MakerBundle with the command bin/console make:state-processor for an easy start.
  2. Manually create a class, such as App\State\UserHashPasswordProcessor, to fulfill this role.

This processor must implement the ProcessorInterface, which defines a process method to create, delete, update, or alter data. Here's how you might implement it:

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;

final class UserPasswordHasherStateProcessor implements ProcessorInterface
{
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
{
// Logic goes here.
}
}

Next, link the User entity to the processor.

<?php

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use App\State\UserPasswordHasherStateProcessor;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;

#[ApiResource(
operations: [
new Post(processor: UserPasswordHasherStateProcessor::class),
new Get(),
]
)]
class User implements PasswordAuthenticatedUserInterface

Thanks to these changes, after the deserialization process of User entity, the UserPasswordHasherProcessor will be called.

Hashing the Password Before Persistence

To achieve our goal, we have to modify the UserPasswordHasherStateProcessor class by incorporating composition. Here is an implementation example:

<?php

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserPasswordHasherStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private ProcessorInterface $persistProcessor,
private UserPasswordHasherInterface $passwordHasher
) {}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
{
if ($data instanceof User) {
$password = $this->passwordHasher->hashPassword($data, $data->getPassword());
$data->setPassword($password);
}

// Ensures the processed data is saved
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}

What have we done here :

  • Doctrine persister service injection: We’ve injected the api_platform.doctrine.orm.state.persist_processor service via autowiring in the constructor, a crucial step for ensuring that changes are persisted.
  • UserPasswordHasherInterface injection: The UserPasswordHasherInterface is also injected to handle password hashing, necessitating the implementation of PasswordAuthenticatedUserInterface by our User entity.

This setup ensures that any password provided during user creation is securely hashed before being stored.

And that’s it. If you understood what we demonstrated here, you have all capabilities to do whatever you want.

A project can include as many state processors as needed. The first able to process the data for a given resource will be used. — Api Platform

Creating a Custom State Provider

Since we know how work state processors, we can do the same with provider.

Suppose we want to avoid exposing our internal model directly through the API. In that case, we can define an output class to represent the data we wish to expose, starting with a DTO, for example.

<?php
namespace App\Dto\Output;

final class UserOutput
{
public function __construct(
public int $id,
public string $email
) {}
}

Then generate a provider class for our entity with bin/console make:state-provider, which creates a template like this:

<?php

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;

final class UserRepresentationProvider implements ProviderInterface
{
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// Logic goes here.
}
}

To operationalize this provider, update the User entity to define the provider and output for the GET operation.

<?php

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use App\Dto\Output\UserOutput;
use App\State\UserRepresentationProvider;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;

#[ApiResource(
operations: [
new Post(processor: UserHashPasswordStateProcessor::class),
# We add provider definition here.
new Get(output: UserOutput::class, provider: UserRepresentationProvider::class),
]
)]
class User implements PasswordAuthenticatedUserInterface

Since we made these changes inside entity, we will need to get the User instance inside the provider to instanciate our DTO.

For that we need to inject the service api_platform.doctrine.orm.state.item_provider hrough autowiring, just like we did with the persister in the processor.

<?php

#...
use App\Dto\Output\UserOutput;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class UserRepresentationProvider implements ProviderInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.item_provider')]
private ProviderInterface $decorated
)
{}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$user = $this->decorated->provide($operation, $uriVariables, $context);
return new UserOutput(
$user->getId(),
$user->getEmail()
);
}
}

Injecting the api_platform.doctrine.orm.state.item_provider service through autowiring, as done with the persister in the processor, is essential for constructing your DTO with real entity values. In this case this ensure sensitive information like password is not exposed.

That’s it. This is exactly what you have to know about state providers and processors!

To go further

This blog has demonstrated how to create custom state processors and providers using API Platform, emphasizing the necessity of service injection for their functionality.

While this covers the basics, there’s much more to explore:

  • Serialization groups: To prevent exposing all attributes, including hashed passwords, consider using serialization groups or implementing a write operation with a different output from the resource.
  • Using DTOs: Adopting DTOs for specific actions (e.g., user creation, password editing) promotes a clean separation of concerns, enhancing security and maintainability.
  • Why Not Controllers?: Leveraging processors and providers allows for a clear separation of concerns, keeping controllers focused on handling HTTP requests and responses, while data manipulation is managed elsewhere, aligning with the MVC architecture’s principles. This approach not only streamlines development but also ensures scalable, maintainable web applications and APIs.

--

--

Etearner

An eternal learner who is passionate about his craft and helping others advance in theirs.