Skip to Content

Chapitre 11.3 — Filtres, pagination & tri

Partie 11 : API Platform — Chapitre 3/8 Version de référence : API Platform 4 / Symfony 7.4.

⏱️ TL;DR

  • API Platform fournit des filtres prêts qu’on active par attribut #[ApiFilter(...)] : SearchFilter (exact/partiel), OrderFilter (tri), RangeFilter (min/max), DateFilter, BooleanFilter, ExistsFilter.
  • Le client filtre via la query string : GET /api/bookings?status=confirmed&order[start]=desc&page=2.
  • La pagination est active par défaut (30 éléments/page) ; configurable (taille, client-contrôlée, désactivable).
  • Pour des logiques sur mesure, on écrit un filtre custom.
  • Pour un dev Next.js : ce sont les query params de filtrage/tri/pagination que votre front passera à l’API — standardisés et documentés dans OpenAPI.

🎯 Objectifs

  • Activer les filtres de recherche, tri et plage.
  • Paginer et contrôler la taille de page.
  • Composer filtres + tri + pagination dans une requête.
  • Savoir quand écrire un filtre custom.

1. Activer un filtre de recherche

On déclare les filtres avec #[ApiFilter(...)] sur la resource, en précisant les propriétés et la stratégie :

use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Doctrine\Orm\Filter\{SearchFilter, OrderFilter, DateFilter}; #[ApiResource] #[ApiFilter(SearchFilter::class, properties: [ 'status' => 'exact', // ?status=confirmed 'customerEmail' => 'partial', // ?customerEmail=zen (LIKE %zen%) 'slot.service.provider' => 'exact', // filtre via relations : ?slot.service.provider=/api/providers/1 ])] #[ApiFilter(DateFilter::class, properties: ['start'])] // ?start[after]=2026-08-01 #[ApiFilter(OrderFilter::class, properties: ['start', 'createdAt'])] // ?order[start]=desc class Booking { /* ... */ }

Stratégies de SearchFilter : exact, partial (LIKE %v%), start/end/word_start. On peut filtrer à travers les relations (slot.service.provider), ce qui est très puissant.

2. Les filtres intégrés

FiltreUsageExemple de query
SearchFilterégalité / LIKE?customerEmail=zen
OrderFiltertri?order[start]=desc
RangeFilterplage numérique?seats[gte]=2&seats[lte]=4
DateFilterplage de dates?start[after]=2026-08-01
BooleanFilterbooléen?active=true
ExistsFilternull / non-null?exists[payment]=true

Chaque filtre déclaré apparaît automatiquement dans la doc OpenAPI/Swagger (chapitre 11.6), avec ses paramètres — le client sait quoi envoyer.

3. La pagination

Elle est active par défaut : GET /api/bookings renvoie la première page (30 éléments), avec les métadonnées de pagination (en JSON-LD : hydra:totalItems, hydra:view avec les liens first/next/last). Le client navigue avec ?page=2.

Configuration (globale ou par resource) :

#[ApiResource( paginationItemsPerPage: 20, // 20 par page paginationClientItemsPerPage: true, // le client peut demander ?itemsPerPage=50 paginationMaximumItemsPerPage: 100, // borne haute (anti-abus) )] class Booking { /* ... */ }

⚠️ Piège : désactiver la pagination (paginationEnabled: false) sur une grande collection = renvoyer des milliers d’objets d’un coup → réponse lourde, mémoire, timeout. Gardez la pagination active ; si un client a besoin de tout, il itère les pages. Et bornez paginationMaximumItemsPerPage pour éviter qu’un client demande ?itemsPerPage=1000000.

4. Composer filtres, tri et pagination

Tout se combine dans la query string :

GET /api/bookings?status=confirmed&start[after]=2026-08-01&order[start]=asc&page=2&itemsPerPage=20

→ les réservations confirmées, à partir du 1er août, triées par date croissante, page 2 de 20. API Platform traduit ça en une requête Doctrine efficace (avec les jointures nécessaires pour les filtres sur relations).

💡 Pour un dev Next.js : côté front, vous construisez ces query params (souvent depuis l’état de l’URL / des search params Next.js). Comme ils sont standardisés et documentés (OpenAPI), un hook de liste (filtres + tri + pagination) se code une fois et se réutilise. Les search params de l’App Router mappent naturellement sur ces filtres.

5. Filtre custom

Pour une logique non couverte (ex. « réservations de la semaine courante », « proches de moi »), on écrit un filtre custom en étendant AbstractFilter : on décrit les paramètres exposés (getDescription) et on modifie le QueryBuilder (filterProperty). C’est là qu’on branche une requête Doctrine sur mesure, tout en gardant l’intégration OpenAPI.

// squelette d'un filtre custom final class CurrentWeekFilter extends AbstractFilter { protected function filterProperty(string $property, $value, QueryBuilder $qb, /* ... */): void { if ($property !== 'currentWeek' || !$value) { return; } $alias = $qb->getRootAliases()[0]; $qb->andWhere("$alias.start BETWEEN :weekStart AND :weekEnd") ->setParameter('weekStart', /* ... */) ->setParameter('weekEnd', /* ... */); } public function getDescription(string $resourceClass): array { return ['currentWeek' => ['property' => 'currentWeek', 'type' => 'bool', 'required' => false]]; } }

✏️ Exercices

Exercice 1. Activez sur Provider : recherche partielle sur name, tri sur name et createdAt.

✅ Solution

#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])] class Provider { /* ... */ }

Requêtes possibles : ?name=zen (LIKE %zen%), ?order[name]=asc, ?order[createdAt]=desc.

Exercice 2. Écrivez la query string pour : services actifs, du prestataire /api/providers/3, triés par nom, page 1 de 50.

✅ Solution

GET /api/services?active=true&provider=/api/providers/3&order[name]=asc&page=1&itemsPerPage=50

(Suppose un BooleanFilter sur active, un SearchFilter exact sur provider, un OrderFilter sur name, et paginationClientItemsPerPage: true avec une borne ≥ 50.)

Exercice 3. Pourquoi ne pas désactiver la pagination sur GET /api/bookings ?

✅ Solution

Parce que la collection peut contenir des milliers de réservations : les renvoyer toutes d’un coup produit une réponse énorme, consomme beaucoup de mémoire côté serveur et client, et peut provoquer des timeouts. On garde la pagination active (le client itère les pages) et on borne paginationMaximumItemsPerPage pour éviter les abus.

🧠 Quiz de révision

1. Comment active-t-on un filtre de recherche ?

Avec #[ApiFilter(SearchFilter::class, properties: ['champ' => 'exact|partial'])] sur la resource ; le client filtre via la query string.

2. Peut-on filtrer via une relation ?

Oui : 'slot.service.provider' => 'exact' permet ?slot.service.provider=/api/providers/1 (API Platform ajoute les jointures).

3. La pagination est-elle active par défaut ?

Oui (30 éléments/page par défaut), avec métadonnées de pagination. Configurable (taille, client-contrôlée, bornée).

4. Comment trie-t-on une collection ?

Avec OrderFilter : ?order[champ]=asc|desc (les champs autorisés sont déclarés).

5. Où trouve-t-on les filtres disponibles pour un client ?

Dans la doc OpenAPI/Swagger : chaque filtre déclaré y apparaît automatiquement avec ses paramètres.


Prochain chapitre : 11.4 — Validation & erreurs — contraintes réutilisées et réponses d’erreur normalisées.