Aller au contenu
Sylvain Joffraud l4w
Retour

API Platform 4 : contrôleurs custom ni sérialisés ni validés

En API Platform 4, un contrôleur custom qui renvoie une entité n’est plus forcément sérialisé ni validé comme en 3.x. Sur le back Symfony d’un SaaS d’abonnements, la montée d’API Platform 3.4.17 à 4.3.3 a cassé trois contrôleurs de ce type, dont celui qui active un abonnement. La correction : valider explicitement avec le validator Symfony, lever la ValidationException d’API Platform, et renvoyer une JsonResponse sérialisée en JSON-LD. Le reste de la migration s’est fait en quatre lots, avec 935 tests et 2 874 assertions identiques avant et après.

Une migration en lots, compatible 3.4 d’abord

PHP 8.4, Symfony 7.4, Doctrine ORM 3.6.3 : la base était à jour, State Processors partout, couche de compatibilité déjà désactivée. J’ai découpé la migration en lots, chacun mergé avec les tests verts :

  1. Configuration, encore en 3.4 : use_symfony_listeners: true, serializer.hydra_prefix: true pour figer explicitement le comportement 3.x, suppression de event_listeners_backward_compatibility_layer et keep_legacy_inflector, namespaces ApiPlatform\Core\* remplacés dans exception_to_status.
  2. Composer : api-platform/core ^3.4 remplacé par api-platform/symfony et api-platform/doctrine-orm ^4.3.
  3. Front : le format Hydra sans préfixe.
  4. Filtres : #[ApiFilter] vers QueryParameter.

L’audit préalable avait listé les risques connus. La migration en a révélé cinq autres :

ChangementAnticipé ?Correctif
ApiPlatformBundle absent de bundles.phpnonajouté : les packages séparés ne l’enregistrent plus
openapiContextnonopenapi: new Operation(...), 4 occurrences
NotFoundActionnonApiPlatform\Action → ApiPlatform\Symfony\Action, 17 entités
ValidationException dans src/nonApiPlatform\Symfony\Validator → ApiPlatform\Validator, 6 fichiers
contrôleurs qui renvoient une entiténonJsonResponse + validation explicite, 3 contrôleurs

Fausse piste au passage : le plan affirmait que SerializerContextBuilderInterface gardait son namespace. Faux, il passe de ApiPlatform\Serializer à ApiPlatform\State.

Pourquoi un contrôleur custom n’est plus sérialisé ni validé en API Platform 4 ?

L’opération d’activation est un Post avec un contrôleur maison, deserialize: false et un groupe de validation full. En 3.4, le contrôleur renvoyait l’entité et API Platform faisait le reste : validation avec les groupes de l’opération, sérialisation JSON-LD avec son normalizationContext.

public function __invoke(Subscription $subscription): Subscription
{
    if (!$subscription->isSimpleWorkflow()) {
        $this->subscriptionPdfGenerator->saveSubscriptionAsPdf($subscription);
    }

    $this->subscriptionService->unDraft($subscription);
    $this->entityManager->flush();

    return $subscription;
}

Après la montée en 4.3, ce retour d’entité n’était plus pris en charge par la chaîne d’API Platform : ni sérialisation automatique, ni validation de l’entité. Le test fonctionnel qui active un abonnement sans client et attend un 422 est exactement le genre de filet qui rend ce changement visible.

En API Platform 4, un contrôleur custom qui renvoie une entité ne peut plus compter sur la validation et la sérialisation implicites : il doit les faire lui-même.

Le correctif : valider et sérialiser explicitement

use ApiPlatform\Validator\Exception\ValidationException;

public function __invoke(Subscription $subscription): JsonResponse
{
    $violations = $this->validator->validate($subscription, null, ['full']);
    if ($violations->count() > 0) {
        throw new ValidationException($violations);
    }

    if (!$subscription->isSimpleWorkflow()) {
        $this->subscriptionPdfGenerator->saveSubscriptionAsPdf($subscription);
    }

    $this->subscriptionService->unDraft($subscription);
    $this->entityManager->flush();

    return new JsonResponse(
        $this->serializer->serialize($subscription, 'jsonld'),
        JsonResponse::HTTP_CREATED,
        [],
        true,
    );
}

La ValidationException d’API Platform reste mappée sur 422 dans exception_to_status, donc le format d’erreur ne change pas pour le front. La validation se fait désormais avant tout effet de bord : génération du PDF du contrat d’abonnement, flush(). Ce PDF est d’ailleurs celui qui m’a valu une EntityNotFoundException dans un listener postRemove.

⚠️ Le normalizationContext de l’opération ne s’applique plus non plus. Le contrôleur qui renvoie l’utilisateur courant passe ses groupes à la main : ['groups' => ['user:get', 'user:current', 'address:get', 'data_source:get']].

Hydra sans préfixe et QueryParameter

En 4.x, les clés JSON-LD perdent le préfixe hydra: : member et totalItems au lieu de hydra:member et hydra:totalItems, @type vaut Collection. Quatre fichiers de tests back ont suivi. Côté front React, les clés étaient centralisées dans un enum, la correction tient en deux lignes :

export enum HydraProps {
  HYDRA_MEMBER = 'member',          // était 'hydra:member'
  HYDRA_TOTAL_ITEMS = 'totalItems', // était 'hydra:totalItems'
}

Dernier lot, optionnel : les cinq entités en #[ApiFilter] passent en QueryParameter dans #[ApiResource] :

parameters: [
    'subscriptions' => new QueryParameter(filter: new IriFilter(), property: 'subscriptions'),
    'type.code' => new QueryParameter(filter: new ExactFilter(), property: 'type.code'),
    'name' => new QueryParameter(filter: new PartialSearchFilter(), property: 'name'),
],

Lessons learned

Questions fréquentes

Pourquoi mon contrôleur custom API Platform 4 ne renvoie plus de JSON-LD ?
Après une migration d’API Platform 3.4 vers 4.3, un contrôleur custom qui renvoie directement une entité peut ne plus être sérialisé par API Platform. La solution la plus explicite consiste à sérialiser soi-même avec le serializer Symfony au format jsonld, à renvoyer une JsonResponse avec le code HTTP voulu, et à passer les groupes de normalisation à la main.
Comment valider une entité dans un contrôleur API Platform 4 ?
Pour valider une entité dans un contrôleur API Platform 4, on injecte le ValidatorInterface de Symfony, on appelle validate($entity, null, ['full']) avec les groupes de l’opération, puis on lève ApiPlatform\Validator\Exception\ValidationException si la liste de violations n’est pas vide. API Platform la convertit en réponse 422 au format d’erreur habituel.
Où sont passés <code>hydra:member</code> et <code>hydra:totalItems</code> en API Platform 4 ?
API Platform 4 supprime par défaut le préfixe hydra: des clés JSON-LD. Une collection renvoie member et totalItems, et son @type devient Collection. Le front doit suivre : si les clés sont centralisées dans un seul enum, la correction tient en deux lignes. En 3.4, l’option serializer.hydra_prefix fixe le comportement explicitement.
Faut-il migrer <code>#[ApiFilter]</code> vers <code>QueryParameter</code> tout de suite ?
La migration de #[ApiFilter] vers QueryParameter n’est pas bloquante pour passer en API Platform 4 : l’attribut reste supporté, déprécié en vue de la version 5. Elle se fait dans un lot séparé, entité par entité, avec un test fonctionnel par filtre pour vérifier que le filtrage renvoie toujours les mêmes résultats.

Partager cet article :

Article précédent
LLM-juge : jugé parmi douze critères, il inverse le verdict
Article suivant
Astro + Svelte : des styles perdus sans erreur de build