Aller au contenu principal

Exposer une collection paginée et filtrable avec DTO

Ce cookbook présente un pattern complet pour :

  • partir d’une entité Doctrine (Customer),
  • exposer une collection paginée via API Platform 4,
  • retourner un DTO avec champs calculés (ex: fullName = firstName + lastName),
  • conserver la puissance des filtres Doctrine (WHERE, ORDER BY) et de la pagination,
  • préparer des implémentations côté frontend (Vue / Flutter) qui consomment cette API proprement.

Les différentes implémentations techniques (Symfony, Vue, Flutter) partagent le même contrat d’API et la même philosophie.

Code source complet

Les exemples complets (structure de répertoires, tests, configuration) sont disponibles dans les dépôts Doing.

Les extraits ci-dessous se concentrent sur la compréhension du pattern et renvoient vers les cookbooks dédiés pour les détails d’implémentation.

Symfony : 1h30 VueJS : 1h Total : 2h30

TL;DR

  1. Backend Symfony :

    • une entité Doctrine Customer reste la ressource principale API Platform,
    • un CustomerDto définit le contrat d’API (avec un champ calculé fullName),
    • un CustomerDtoProvider remplace les entités par des DTO tout en conservant pagination + filtres,
    • des QueryParameter modernes branchés sur des filtres Doctrine permettent de filtrer / trier sur fullName et age.
  2. Frontend (Vue / Flutter, à venir) :

    • consomme /api/customers paginé,
    • utilise des query params fullName, age, order[fullName],
    • affiche une liste réactive de clients avec recherche et tri.

Le cœur de la complexité est côté backend : comment rester sur une ressource Doctrine, tout en exposant un DTO riche et filtrable sur des champs calculés.

Cas d’usage couvert

Cas d’usage typique back-office :

  • la base contient une entité Customer avec les “vrais” champs métier : firstName, lastName, age, city ;
  • l’API publique ne doit pas exposer directement cette entité (contrat d’API indépendant de la persistence) ;
  • on veut un DTO avec un champ calculé fullName et éventuellement d’autres champs dérivés ;
  • on veut filtrer/ordonner sur fullName comme si c’était un champ natif ;
  • on veut garder tout le confort d’API Platform : pagination, filtres Doctrine, documentation OpenAPI.
Quand adopter ce pattern ?

Adopte ce pattern lorsque :

  • ton modèle Doctrine ne correspond pas exactement au contrat d’API souhaité,
  • tu dois exposer des champs calculés (nom complet, âge, labels combinés…),
  • tu veux préserver les extensions Doctrine (filtres globaux, sécurité, pagination) d’API Platform,
  • tu anticipes du versionning d’API (DTO plus stable que l’entité).

Architecture globale

  • API Platform pilote les opérations (Get / GetCollection), la pagination et les filtres.
  • Doctrine exécute les requêtes SQL et applique les filtros (WHERE, ORDER BY).
  • Un state provider custom fait le mapping entité → DTO.
  • Le client (Vue / Flutter / autre) ne voit que le DTO, jamais l’entité.

Vue d’ensemble des paramètres de recherche

La collection d’exemple /api/customers accepte plusieurs paramètres de requête :

ParamètreExemple d’appelEffet côté backend
fullName/api/customers?fullName=dupontFiltre sur CONCAT(firstName, ' ', lastName) (LIKE)
age/api/customers?age=30Filtre sur le champ age égal à un entier (cast auto)
order[fullName]/api/customers?order[fullName]=ascTri par nom complet (ASC/DESC) via ORDER BY calculé

Côté API Platform 4.2+, ces paramètres sont déclarés avec QueryParameter sur l’opération GetCollection et associés à des filtres Doctrine (FilterInterface).

Pour le détail pas-à-pas (signature des filtres, gestion du QueryBuilder), voir la partie Symfony :

Symfony - Exposer une collection paginée et filtrable avec DTO

Exemple rapide d’appels HTTP

# Liste paginée par défaut
curl "https://api.example.test/api/customers"

# Filtre sur le nom complet
curl "https://api.example.test/api/customers?fullName=dupont"

# Filtre combiné nom complet + âge
curl "https://api.example.test/api/customers?fullName=jean&age=30"

# Tri ascendant sur le nom complet
curl "https://api.example.test/api/customers?order[fullName]=asc"

# Filtre + tri
curl "https://api.example.test/api/customers?fullName=dupont&order[fullName]=asc"

Les réponses sont paginées selon la configuration API Platform (par défaut 20 éléments/page dans l’exemple Symfony) et exposent les métadonnées habituelles (liens next / previous, total, etc.).


➡ Pour entrer dans le détail de l’implémentation backend : symfony.mdx.

➡ Pour les intégrations front (Vue / Flutter), les cookbooks dédiés seront ajoutés prochainement.