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
| Filtre | Usage | Exemple de query |
|---|---|---|
SearchFilter | égalité / LIKE | ?customerEmail=zen |
OrderFilter | tri | ?order[start]=desc |
RangeFilter | plage numérique | ?seats[gte]=2&seats[lte]=4 |
DateFilter | plage de dates | ?start[after]=2026-08-01 |
BooleanFilter | booléen | ?active=true |
ExistsFilter | null / 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 bornezpaginationMaximumItemsPerPagepour é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.