Api Platform conference
Register now

To mutate the application states during POST, PUT, PATCH or DELETE operations, API Platform uses classes called state processors. State processors receive an instance of the class marked as an API resource (usually using the #[ApiResource] attribute). This instance contains data submitted by the client during the deserialization process.

With the Symfony variant, a state processor using Doctrine ORM is included with the library and is enabled by default. It is able to persist and delete objects that are also mapped as Doctrine entities. A Doctrine MongoDB ODM state processor is also included and can be enabled by following the MongoDB documentation.

With the Laravel variant, a state processor using Eloquent ORM is included with the library and is enabled by default. It is able to persist and delete objects that are also mapped as Related Models.

However, you may want to:

  • store data to other persistence layers (Elasticsearch, external web services…)
  • not publicly expose the internal model mapped with the database through the API
  • use a separate model for read operations and for updates by implementing patterns such as CQRS

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

# Creating a Custom State Processor

# Custom State Processor with Symfony

If the Symfony MakerBundle is installed in your project, you can use the following command to generate a custom state processor easily:

bin/console make:state-processor

To create a state processor, you have to implement the ProcessorInterface. This interface defines a method process: to create, delete, update, or alter the given data in any ways.

Here is an implementation example:

<?php
// api/src/State/BlogPostProcessor.php

namespace App\State;

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

/**
 * @implements ProcessorInterface<BlogPost, BlogPost|void>
 */
final class BlogPostProcessor implements ProcessorInterface
{
    /**
     * @return BlogPost|void
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        // call your persistence layer to save $data
        return $data;
    }
}

The process() method must return the created or modified object, or nothing (that’s why void is allowed) for DELETE operations. The process() method can also take an object as input, in the $data parameter, that isn’t of the same type that its output (the returned object). See the DTO documentation entry for more details.

We then configure our operation to use this processor:

<?php
// api/src/Entity/BlogPost.php

namespace App\Entity;

use ApiPlatform\Metadata\Post;
use App\State\BlogPostProcessor;

#[Post(processor: BlogPostProcessor::class)]
class BlogPost {}

# Custom State Processor with Laravel

Using Laravel Artisan Console, you can generate a custom state processor easily with the following command:

php artisan make:state-processor

To create a state processor, you have to implement the ProcessorInterface. This interface defines a method process: to create, delete, update, or alter the given data in any ways.

Here is an implementation example:

<?php
// api/app/State/BlogPostProcessor.php

namespace App\State;

use App\Models\BlogPost;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;

/**
 * @implements ProcessorInterface<BlogPost, BlogPost|void>
 */
final class BlogPostProcessor implements ProcessorInterface
{
    /**
     * @return BlogPost|void
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        // call your persistence layer to save $data
        return $data;
    }
}

The process() method must return the created or modified object, or nothing (that’s why void is allowed) for DELETE operations. The process() method can also take an object as input, in the $data parameter, that isn’t of the same type that its output (the returned object). See the DTO documentation entry for more details.

We then configure our operation to use this processor:

<?php
// api/app/Models/BlogPost.php

namespace App\Models;

use ApiPlatform\Metadata\Post;
use App\State\BlogPostProcessor;

#[Post(processor: BlogPostProcessor::class)]
class BlogPost {}

# Running a Processor on Read (GET) Operations

By default, a custom processor only runs on operations that mutate state: POST, PUT, PATCH, and DELETE. The internal WriteProcessor is responsible for dispatching to your processor, and it skips that step when the operation’s write flag is false.

For safe HTTP methods (GET, GetCollection), the write flag defaults to false — so even if you configure a processor: on a GetCollection, it will not be called unless you explicitly opt in.

To run a processor on a GET or GetCollection operation, set write: true on that operation.

This pattern fits naturally into a CQRS design (mentioned at the top of this page): the processor acts as the command/query handler while the provider covers the read (query) side. With write: true, a processor can serve as the handler for a read operation — for example to record that a collection was accessed, dispatch a domain event, or feed results from a non-standard source that also produces side-effects.

<?php
// api/src/Entity/Car.php

namespace App\Entity;

use ApiPlatform\Metadata\GetCollection;
use App\State\CarProcessor;

#[GetCollection(processor: CarProcessor::class, write: true)]
class Car
{
    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Car:
        operations:
            ApiPlatform\Metadata\GetCollection:
                processor: App\State\CarProcessor
                write: true

The corresponding processor receives the data returned by the provider and can transform or act on it:

<?php
// api/src/State/CarProcessor.php

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;

/**
 * @implements ProcessorInterface<mixed, mixed>
 */
final class CarProcessor implements ProcessorInterface
{
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        // $data is the collection returned by the provider.
        // Add side-effects or transformations here, then return the data.
        return $data;
    }
}

# Hooking into the Built-In State Processors

# Symfony State Processor mechanism

If you want to execute custom business logic before or after persistence, this can be achieved by using composition.

Here is an implementation example which uses Symfony Mailer to send new users a welcome email after a REST POST or GraphQL create operation, in a project using the native Doctrine ORM state processor:

<?php
// api/src/State/UserProcessor.php

namespace App\State;

use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\MailerInterface;

/**
 * @implements ProcessorInterface<User, User|void>
 */
final class UserProcessor implements ProcessorInterface
{
    public function __construct(
        #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
        private ProcessorInterface $persistProcessor,
        #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')]
        private ProcessorInterface $removeProcessor,
        private MailerInterface $mailer,
    )
    {
    }

    /**
     * @return User|void
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        if ($operation instanceof DeleteOperationInterface) {
            return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
        }

        $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
        $this->sendWelcomeEmail($data);

        return $result;
    }

    private function sendWelcomeEmail(User $user): void
    {
        // Your welcome email logic...
        // $this->mailer->send(...);
    }
}

The Autowire attribute is used to inject the built-in processor services registered by API Platform.

If you’re using Doctrine MongoDB ODM instead of Doctrine ORM, replace orm by odm in the name of the injected services.

Finally, configure that you want to use this processor on the User resource:

<?php
// api/src/Entity/User.php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\State\UserProcessor;

#[ApiResource(processor: UserProcessor::class)]
class User {}

# Laravel State Processor mechanism

If you want to execute custom business logic before or after persistence, this can be achieved by using composition.

Here is an implementation example which uses Laravel Mail to send new users a welcome email after a REST POST or GraphQL create operation, in a project using the native Eloquent ORM state processor:

<?php
// api/app/State/UserProcessor.php

namespace App\State;

use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Models\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\MailerInterface;

/**
 * @implements ProcessorInterface<User, User|void>
 */
final class UserProcessor implements ProcessorInterface
{
    public function __construct(
        private ProcessorInterface $persistProcessor,
        private ProcessorInterface $removeProcessor,
    )
    {
    }

    /**
     * @return User|void
     */
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        if ($operation instanceof DeleteOperationInterface) {
            return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
        }

        $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
        $this->sendWelcomeEmail($data);

        return $result;
    }

    private function sendWelcomeEmail(User $user): void
    {
        // Your welcome email logic...
        // Mail::to($user->getEmail())->send(new WelcomeMail($user));
    }
}

Next, we bind the PersistProcessor and RemoveProcessor in our Service Provider:

<?php
// app/Providers/AppServiceProvider.php

namespace App\Providers;

use App\State\UserProcessor;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Foundation\Application;
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->tag(UserProcessor::class, ProcessorInterface::class);

        $this->app->singleton(UserProcessor::class, function (Application $app) {
            return new UserProcessor(
                $app->make(PersistProcessor::class),
                $app->make(RemoveProcessor::class),
            );
        });
    }

    // ...
}

Finally, configure that you want to use this processor on the User resource:

<?php
// api/app/Models/User.php

namespace App\Models;

use ApiPlatform\Metadata\ApiResource;
use App\State\UserProcessor;

#[ApiResource(processor: UserProcessor::class)]
class User {}

# Registering Services Without Autowiring (only for the Symfony variant)

The previous examples work because service autowiring and autoconfiguration are enabled by default in Symfony and API Platform. If you disabled this feature, you need to register the services by yourself and add the api_platform.state_processor tag.

# api/config/services.yaml

services:
    # ...
    App\State\BlogPostProcessor:
        tags: ["api_platform.state_processor"]

    App\State\UserProcessor:
        arguments:
            $persistProcessor: "@api_platform.doctrine.orm.state.persist_processor"
            $removeProcessor: "@api_platform.doctrine.orm.state.remove_processor"
            # If you're using Doctrine MongoDB ODM, you can use the following code:
            # $persistProcessor: '@api_platform.doctrine_mongodb.odm.state.persist_processor'
            # $removeProcessor: '@api_platform.doctrine_mongodb.odm.state.remove_processor'
            $mailer: "@mailer"
        tags: ["api_platform.state_processor"]

You can also help us improve the documentation of this page.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop