API Platform - Les State Providers

Introduction

Dans le TD4 nous vu qu’il existe deux types de classes importantes dans API Platform : Les StateProcessor et les StateProvider.

En résumé, un StateProvider permet de récupérer et traiter l’objet avant traitement, et un StateProcessor de traiter l’objet après l’envoi des données par client et avant sa sauvegarde.

Durant le TD, nous avons utilisé les states processors mais pas de state providers. Ces services peuvent être utiles dans divers cas :

Bref, cet outil est aussi utile que les state processors.

Manipuler les states providers

API Platform fournit une commande pour créer la base d’un StateProvider :

php bin/console make:state-provider MonProvider

Ce qui créé la classe MonProvider dans le dossier src/State.

Il ne reste alors plus qu’à la compléter :

namespace App\State;

class MonProvider implements ProviderInterface
{
    //Injections de dépendances possibles...
    public function __construct()
    {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        //Traitement...
        return $objet;
    }
}

Comme pour les state processors il est possible de décorer un state provider pour utiliser le traitement par défaut puis rajouter sa logique, si besoin :

namespace App\State;

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

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        $objet = $this->provider->provide($operation, $uriVariables, $context);
        //Traitement...
        return $objet;
    }
}

Exemple

Prenons l’exemple suivant :

namespace App\Entity;

class Film {
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $titre = null;

    #[ORM\Column]
    private ?int $anneeSortie = null;
}
namespace App\Entity;

class Ville {
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $nom = null;

    #[ORM\Column]
    private ?int $codePostal = null;

    /**
     * @var Collection<int, Film>
     */
    #[ORM\ManyToMany(targetEntity: Cinema::class, inversedBy: 'filmsDiffuses')]
    private Collection $cinemasDiffusion;
}
namespace App\Entity;

class Cinema {
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $nom = null;

    #[ORM\Column(length: 255)]
    private ?string $adresse = null;

    #[ORM\ManyToOne]
    private ?Ville $ville = null;

    /**
     * @var Collection<int, Film>
     */
    #[ORM\ManyToMany(targetEntity: Film::class, mappedBy: 'cinemasDiffusion')]
    private Collection $filmsDiffuses;
}

On souhaite disposer d’une route : /api/villes/{idVille}/cinemas/films qui permet de retrouver tous les films qui sont diffusés dans les cinémas d’une ville, triées du plus récent au plus ancien. De base, API Platform ne peut pas traiter cette requête, même avec une sous-ressource. Nous allons donc devoir traiter cela avec un state provider customisé, qui effectue une requête DQL :

namespace App\Repository;

use App\Entity\Film;
use App\Entity\Ville;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends ServiceEntityRepository<Film>
*/
class FilmRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Film::class);
    }

    /**
    * @return Film[]
    */
    public function findFilmsQuiPassentDansVille(Ville $ville) : array {
        $queryBuilder = $this->createQueryBuilder('f')
            ->join('f.cinemasDiffusion', 'c')
            ->join('c.ville', 'v')
            ->where('v.id = :idVille')
            ->orderBy('f.anneeSortie', 'DESC')
            ->setParameter('idVille', $ville->getId());
        $query = $queryBuilder->getQuery();
        return $query->getResult();
    }
}
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\FilmRepository;
use App\Repository\VilleRepository;

class FilmVilleProvider implements ProviderInterface
{
    public function __construct(
        private VilleRepository $villeRepository,
        private FilmRepository $filmRepository
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        $ville = $this->villeRepository->findOneBy(['id' => $uriVariables['idVille']]);
        if($ville != null) {
            return $this->filmRepository->findFilmsQuiPassentDansVille($ville);
        }
        //Provoque une erreur 404 si retourne null dans un GetCollection
        return null;
    }
}
namespace App\Entity;

use App\State\FilmVilleProvider;

#[ApiResource(
    uriTemplate: '/villes/{idVille}/cinemas/films',
    operations: [
        new GetCollection()
    ],
    uriVariables: [
        "idVille" => new Link(fromClass: Ville::class)
    ],
    provider: FilmVilleProvider::class
)]
class Film
{
    ...
}

Conclusion

Les state providers sont donc très pratiques afin de proposer des routes particulières où les mécanismes de base d’API Platform ne suffisent pas à réaliser le traitement désiré.

Vous pouvez trouver plus d’information sur la documentation officielle.