API Platform avec Symfony en 2026 : Architecture, State Providers et Questions d'Entretien

Maîtriser API Platform 4.2 avec Symfony : State Providers, Processors, Object Mapper, JSON Streamer pour des performances optimales, et questions d'entretien technique pour développeurs confirmés.

Diagramme d'architecture API Platform Symfony avec workflow REST API

API Platform 4.2 révolutionne la manière dont les applications Symfony exposent des API REST et GraphQL. Cette version introduit le Symfony Object Mapper pour une séparation nette des ressources, le JSON Streamer pour des gains de performance significatifs, et un système de filtres repensé. Pour les développeurs qui préparent des entretiens techniques, la compréhension de ces patterns architecturaux distingue les profils seniors des profils juniors.

Prérequis API Platform 4.2

API Platform 4.2 nécessite Symfony 7.4 ou 8.0. Le support de Symfony 6.4 et 7.0-7.3 a été abandonné. Le JSON Streamer offre jusqu'à 32% de requêtes supplémentaires par seconde sur les endpoints de collection.

Installation d'API Platform 4.2 avec Symfony

API Platform s'installe via Symfony Flex avec une configuration automatique. La configuration par défaut couvre la plupart des cas d'usage tout en restant entièrement personnalisable pour les exigences métier complexes.

bash
# Install API Platform
composer require api-platform/symfony

# The API documentation is available at /api/
# Open http://localhost:8000/api/ after starting the server
symfony serve

La recette Flex configure les groupes de sérialisation, l'intégration Doctrine et la génération de documentation OpenAPI. Les ressources API exposent des opérations CRUD en ajoutant un simple attribut aux classes d'entités.

src/Entity/Book.phpphp
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Put(security: "is_granted('ROLE_ADMIN')"),
        new Delete(security: "is_granted('ROLE_ADMIN')")
    ],
    normalizationContext: ['groups' => ['book:read']],
    denormalizationContext: ['groups' => ['book:write']]
)]
class Book
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Groups(['book:read', 'book:write'])]
    private string $title;

    #[ORM\Column(type: 'text')]
    #[Groups(['book:read', 'book:write'])]
    private string $description;

    #[ORM\Column]
    #[Groups(['book:read'])]
    private \DateTimeImmutable $createdAt;

    // Getters and setters...
}

Cette configuration génère cinq endpoints avec validation automatique, sérialisation et documentation OpenAPI.

State Providers : Récupération de Données depuis Toutes Sources

Les State Providers contrôlent la manière dont API Platform récupère les données pour les opérations GET. Le provider Doctrine par défaut gère la récupération des entités, mais les providers personnalisés permettent l'intégration avec des API externes, Elasticsearch ou des données en cache.

src/State/BookStateProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\BookRepository;
use Psr\Cache\CacheItemPoolInterface;

final class BookStateProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private CacheItemPoolInterface $cache
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        // Single item retrieval
        if (isset($uriVariables['id'])) {
            $cacheKey = sprintf('book_%d', $uriVariables['id']);
            $item = $this->cache->getItem($cacheKey);
            
            if ($item->isHit()) {
                return $item->get();
            }
            
            $book = $this->repository->find($uriVariables['id']);
            $item->set($book)->expiresAfter(3600);
            $this->cache->save($item);
            
            return $book;
        }

        // Collection retrieval with custom filtering
        return $this->repository->findActiveBooks();
    }
}

L'enregistrement du provider sur des opérations spécifiques s'effectue ainsi :

php
#[ApiResource(
    operations: [
        new GetCollection(provider: BookStateProvider::class),
        new Get(provider: BookStateProvider::class),
        // Other operations use default Doctrine provider
        new Post(),
        new Put(),
    ]
)]
class Book { /* ... */ }

State Processors : Gestion des Mutations avec Logique Métier

Les State Processors gèrent les opérations POST, PUT, PATCH et DELETE. Ils reçoivent les données désérialisées et appliquent la logique métier avant la persistance.

src/State/BookStateProcessor.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

final class BookStateProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $em,
        private MailerInterface $mailer,
        private ProcessorInterface $persistProcessor // Decorated Doctrine processor
    ) {}

    public function process(
        mixed $data,
        Operation $operation,
        array $uriVariables = [],
        array $context = []
    ): mixed {
        // Pre-persist business logic
        if ($data instanceof Book && $operation instanceof Post) {
            $data->setCreatedAt(new \DateTimeImmutable());
            $data->setSlug($this->generateSlug($data->getTitle()));
        }

        // Delegate to Doctrine processor
        $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);

        // Post-persist notification
        if ($operation instanceof Post) {
            $this->notifyNewBook($data);
        }

        return $result;
    }

    private function generateSlug(string $title): string
    {
        return strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $title));
    }

    private function notifyNewBook(Book $book): void
    {
        $email = (new Email())
            ->to('catalog@example.com')
            ->subject('New book added: ' . $book->getTitle())
            ->text('A new book has been added to the catalog.');
        $this->mailer->send($email);
    }
}
Décoration des Processors

La décoration du processor Doctrine par défaut avec #[AsDecorator] préserve le comportement de persistance tout en ajoutant une logique personnalisée. Ce pattern évite de dupliquer les opérations ORM.

Object Mapper : Séparation des Ressources API et des Entités

API Platform 4.2 intègre le composant Symfony Object Mapper pour découpler les représentations API des entités du domaine. Cette séparation permet différents modèles de lecture/écriture et protège les structures internes des entités.

src/ApiResource/BookResource.phpphp
namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Book;
use Symfony\Component\ObjectMapper\Attribute\Map;

#[ApiResource(
    shortName: 'Book',
    operations: [
        new GetCollection(),
        new Get()
    ]
)]
#[Map(target: Book::class)]
class BookResource
{
    public ?int $id = null;
    
    public string $title;
    
    public string $description;
    
    // Computed field not in entity
    public int $wordCount;
    
    // Formatted date for API consumers
    public string $publishedDate;
}

Le provider mapper transforme automatiquement les entités en ressources :

src/State/BookResourceProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\BookResource;
use App\Repository\BookRepository;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

final class BookResourceProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private ObjectMapperInterface $mapper
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables['id'])) {
            $book = $this->repository->find($uriVariables['id']);
            return $book ? $this->toResource($book) : null;
        }

        return array_map(
            fn(Book $book) => $this->toResource($book),
            $this->repository->findAll()
        );
    }

    private function toResource(Book $book): BookResource
    {
        $resource = $this->mapper->map($book, BookResource::class);
        $resource->wordCount = str_word_count($book->getDescription());
        $resource->publishedDate = $book->getCreatedAt()->format('F j, Y');
        return $resource;
    }
}

Prêt à réussir tes entretiens Symfony ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

JSON Streamer : Amélioration des Performances de 32%

Le composant JSON Streamer sérialise les grandes collections sans charger l'ensemble des données en mémoire. Les benchmarks sur l'API Sylius montrent une augmentation de 32,4% des requêtes par seconde.

Activation du streaming au niveau de la ressource ou de l'opération :

php
#[ApiResource(
    operations: [
        new GetCollection(
            jsonStream: true,  // Enable JSON streaming
            paginationItemsPerPage: 100
        ),
        new Get()
    ]
)]
class Book { /* ... */ }

Le streaming bénéficie particulièrement aux :

  • Endpoints de collection avec plus de 50 éléments
  • Ressources avec des relations imbriquées
  • API servant des clients mobiles avec bande passante limitée

La spécification OpenAPI a également été optimisée. La mutualisation des JSON Schema réduit la taille des fichiers de 30%, améliorant les temps de chargement de la documentation.

Questions d'Entretien : Architecture API Platform

Les entretiens techniques pour les postes Symfony couvrent fréquemment les patterns API Platform. Ces questions évaluent la compréhension de l'architecture du framework au-delà des opérations CRUD basiques.

"Expliquez la différence entre State Providers et Processors"

Réponse attendue : Les State Providers récupèrent les données pour les opérations de lecture (GET). Ils retournent des entités, des DTOs ou des tableaux. Les State Processors gèrent les opérations d'écriture (POST, PUT, PATCH, DELETE). Ils reçoivent les données désérialisées et exécutent la logique métier avant la persistance. Cette séparation suit les principes CQRS : les requêtes passent par les Providers, les commandes par les Processors.

"Quand utiliser une ressource API personnalisée plutôt qu'exposer directement une entité ?"

Réponse attendue : Les ressources personnalisées s'appliquent lorsque :

  • La représentation API diffère du schéma de base de données
  • Des champs calculés nécessitent l'agrégation de plusieurs entités
  • Les modèles de lecture et d'écriture requièrent des structures différentes
  • Des champs internes des entités doivent rester cachés des consommateurs de l'API
  • La compatibilité de version exige des contrats stables pendant que les entités évoluent

"Comment API Platform gère-t-il la validation ?"

Réponse attendue : API Platform utilise les contraintes Symfony Validator sur les propriétés des entités. La validation s'exécute automatiquement pendant la désérialisation avant l'exécution du State Processor. Les groupes de validation contrôlent quelles contraintes s'appliquent par opération. Les validateurs personnalisés s'intègrent via les mécanismes Symfony standards.

php
#[ApiResource(
    operations: [
        new Post(validationContext: ['groups' => ['create']]),
        new Put(validationContext: ['groups' => ['update']])
    ]
)]
class Book
{
    #[Assert\NotBlank(groups: ['create', 'update'])]
    private string $title;

    #[Assert\Isbn(groups: ['create'])]
    private string $isbn;  // Required only on creation
}
Erreur Courante en Entretien

Les candidats décrivent souvent la validation comme "automatique" sans mentionner les groupes de validation ou les contraintes personnalisées. Les recruteurs recherchent une compréhension de la personnalisation de la validation par opération.

"Quels mécanismes de sécurité API Platform fournit-il ?"

Réponse attendue : API Platform s'intègre avec Symfony Security via :

  • L'attribut security sur les opérations pour le contrôle d'accès basé sur les rôles
  • securityPostDenormalize pour les vérifications au niveau objet après la liaison des données
  • Les Voters pour une logique d'autorisation complexe
  • La limitation de débit via l'intégration du Symfony Rate Limiter
php
#[ApiResource(
    operations: [
        new Get(
            security: "is_granted('ROLE_USER')"
        ),
        new Put(
            security: "is_granted('ROLE_ADMIN') or object.getOwner() == user",
            securityPostDenormalize: "is_granted('BOOK_EDIT', object)"
        )
    ]
)]

Filtres et Pagination : Patterns de Requêtes Avancés

Les filtres API Platform permettent aux clients d'interroger les collections avec des paramètres URL. Le système de filtres dans la version 4.2 a été repensé pour une meilleure extensibilité.

php
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
    'title' => 'partial',      // LIKE %value%
    'author.name' => 'exact'   // Nested property
])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
#[ApiFilter(OrderFilter::class, properties: ['title', 'createdAt'])]
class Book { /* ... */ }

Endpoints générés :

text
GET /api/books?title=symfony           # Search by title
GET /api/books?createdAt[after]=2026-01-01  # Date range
GET /api/books?order[createdAt]=desc   # Sorting

Tests des Ressources API Platform

API Platform fournit un client de test qui simplifie les tests fonctionnels. La classe ApiTestCase offre des assertions spécifiques aux réponses API.

tests/Api/BookTest.phpphp
namespace App\Tests\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;

class BookTest extends ApiTestCase
{
    public function testGetCollection(): void
    {
        $response = static::createClient()->request('GET', '/api/books');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
        $this->assertJsonContains([
            '@context' => '/api/contexts/Book',
            '@type' => 'Collection'
        ]);
    }

    public function testCreateBook(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => [
                'title' => 'Symfony Best Practices',
                'description' => 'A guide to modern Symfony development'
            ],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(201);
        $this->assertJsonContains(['title' => 'Symfony Best Practices']);
    }

    public function testCreateBookValidationFails(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => ['description' => 'Missing title'],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(422);
        $this->assertJsonContains([
            'violations' => [
                ['propertyPath' => 'title', 'message' => 'This value should not be blank.']
            ]
        ]);
    }
}

Points Clés pour API Platform avec Symfony

  • Les State Providers gèrent les opérations GET, les State Processors gèrent les mutations. Cette séparation permet une architecture propre avec des sources de données personnalisées et une logique métier
  • Le composant Object Mapper découple les ressources API des entités Doctrine, permettant différents modèles de lecture/écriture et protégeant les structures internes
  • Le JSON Streamer offre une amélioration des performances de 32% sur les endpoints de collection en sérialisant sans allocation mémoire complète
  • La sécurité s'intègre via les mécanismes standards de Symfony : expressions security, Voters et composant Rate Limiter
  • Les groupes de validation personnalisent l'application des contraintes par opération
  • Les filtres exposent automatiquement des paramètres de requête, les filtres de recherche, date et tri couvrant la plupart des cas d'usage
  • Les questions d'entretien se concentrent sur les décisions architecturales : quand utiliser des providers personnalisés, comment séparer les modèles de lecture/écriture, et les patterns d'implémentation de la sécurité

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Défi du jour

Tu saurais repérer le bug en Symfony ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 8 septembre 2026

Tags

#api-platform
#symfony
#rest-api
#state-providers
#interview

Partager

Articles similaires