- Créer une ressource avec des relations
- JMSSerializer : Une alternative au sérialiseur natif de Symfony
Jusqu’à présent les query strings ou paramètres d’URL ont été recalés dans tous les choix de conception que nous avons déjà faits.
Ces composants à part entière du protocole HTTP peuvent être exploités dans une API REST pour atteindre différents objectifs.
Dans cette partie, nous allons aborder quelques cas pratiques où les query strings peuvent être utilisés.
Tout au long de cette partie, le terme query strings sera utilisé pour désigner les paramètres d’URL.
- Pourquoi utiliser les query strings ?
- Gestion des query strings avec FOSRestBundle
- Paginer et Trier les réponses
Pourquoi utiliser les query strings ?
Le premier cas d’usage qui est assez courant lorsque nous utilisons ou nous développons une API est la pagination ou le filtrage des réponses que nous obtenons.
Nous pouvons actuellement lister tous les lieux ou tous les utilisateurs de notre application. Cette réponse peut rapidement poser des problèmes de performance si ces listes grossissent dans le temps.
Comment alors récupérer notre liste de lieux tout en réduisant/filtrant cette liste alors que nous avons un seul appel permettant de lister les lieux de notre application : GET rest-api.local/places
?
La liste de lieux est une ressource avec un identifiant places
. Pour récupérer cette même liste tout en conservant son identifiant nous ne pouvons pas modifier l’URL.
Par contre, les query string nous permettent de pallier à ce genre de problèmes.
Donc au sein d’une même URL (ici rest-api.local/places
), nous pouvons rajouter des query strings afin d’obtenir des réponses différentes mais qui représentent toutes une liste de lieux.
Gestion des query strings avec FOSRestBundle
Avant d’aborder les cas pratiques, nous allons commencer par voir comment FOSRestBundle nous permet de définir les query strings.
Le framework Symfony supporte de base les query strings mais FOSRestBundle rajoute beaucoup de fonctionnalités comme :
- définir des règles de validation pour ce query string ;
- définir une valeur par défaut ;
- et beaucoup d’autres fonctionnalités.
L’annotation QueryParam
Pour accéder à toutes ces fonctionnalités, il suffit d’utiliser une annotation FOS\RestBundle\Controller\Annotations\QueryParam
sur le ou les actions de nos contrôleurs.
Il est aussi possible d’utiliser cette annotation sur un contrôleur mais nous ne parlerons pas de ce cas d’usage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php /** * @QueryParam( * name="", * key=null, * requirements="", * incompatibles={}, * default=null, * description="", * strict=false, * array=false, * nullable=false * ) */ |
Nous aborderons les cas d’utilisation des attributs de cette annotation dans la suite.
Le listener
Pour dire à FOSRestBundle de traiter cette annotation, nous devons activer un listener dédié appelé le Param Fetcher Listener. Pour ce faire, nous allons modifier le fichier de configuration :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # app/config/config.yml # ... fos_rest: routing_loader: include_format: false view: view_response_listener: true formats: json: true xml: true format_listener: rules: - { path: '^/', priorities: ['json', 'xml'], fallback_format: 'json', prefer_extension: false } body_listener: enabled: true param_fetcher_listener: enabled: true # ... |
Maintenant que le listener est activé, nous pouvons passer aux choses sérieuses.
Paginer et Trier les réponses
Paginer la liste de lieux
Commençons par mettre en place une pagination pour la liste des lieux. Pour obtenir cette pagination, nous allons utiliser un principe simple.
Deux query strings vont permettre de choisir l’index du premier résultat souhaité (offset
) et le nombre de résultats souhaités (limit
).
Ces deux paramètres sont facultatifs mais doivent obligatoirement être des entiers positifs.
Pour implémenter ce fonctionnement, il suffit de rajouter deux annotations QueryParam
dans l’action qui liste les lieux.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <?php # src/AppBundle/Controller/PlaceController.php namespace AppBundle\Controller; // ... use FOS\RestBundle\Controller\Annotations\QueryParam; // ... class PlaceController extends Controller { /** * @Rest\View(serializerGroups={"place"}) * @Rest\Get("/places") * @QueryParam(name="offset", requirements="\d+", default="", description="Index de début de la pagination") * @QueryParam(name="limit", requirements="\d+", default="", description="Index de fin de la pagination") */ public function getPlacesAction(Request $request) { $places = $this->get('doctrine.orm.entity_manager') ->getRepository('AppBundle:Place') ->findAll(); /* @var $places Place[] */ return $places; } // ... } |
Avec l’attribut requirements
, nous utilisons une expression régulière pour valider les paramètres. Si les données ne sont pas valides alors le paramètre vaudra sa valeur par défaut (une chaîne vide pour notre cas). Si les paramètres ne sont pas renseignés, ils seront aussi vides.
Il faut aussi noter que nous pouvons utiliser n’importe quelle valeur par défaut. En effet, elle n’est pas validée par FOSRestBundle. C’est d’ailleurs pour cette raison que nous pouvons mettre dans notre exemple une chaîne vide comme valeur par défaut alors que notre expression régulière ne valide que les entiers.
L’expression régulière utilisée dans requirements
est traité en rajoutant automatiquement un pattern du type #^notre_regex$#xsu
. En mettant, \d+
nous validons donc avec #^\d+$#xsu
. Vous pouvez consulter la documentation de PHP pour voir l’utilité des options x
(ignorer les caractères d’espacement), s
(pour utiliser .
comme métacaractère générique) et u
(le masque et la chaîne d’entrée sont traitées comme des chaînes UTF-8.).
Pour les traiter, nous avons plusieurs choix. Nous pouvons utiliser un attribut de l’objet Request appelé paramFetcher
que le Param Fetcher Listener crée automatiquement. Ou encore, nous pouvons ajouter un paramètre à notre action qui doit être du type FOS\RestBundle\Request\ParamFetcher
.
Avec la cette dernière méthode, que nous allons utiliser, le Param Fetcher Listener injecte automatiquement le param fetcher à notre place.
L’objet ainsi obtenu permet d’accéder aux différents query strings que nous avons déclarés.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php # src/AppBundle/Controller/PlaceController.php namespace AppBundle\Controller; // ... use FOS\RestBundle\Controller\Annotations\QueryParam; use FOS\RestBundle\Request\ParamFetcher; // ... class PlaceController extends Controller { /** * @Rest\View(serializerGroups={"place"}) * @Rest\Get("/places") * @QueryParam(name="offset", requirements="\d+", default="", description="Index de début de la pagination") * @QueryParam(name="limit", requirements="\d+", default="", description="Nombre d'éléments à afficher") */ public function getPlacesAction(Request $request, ParamFetcher $paramFetcher) { $offset = $paramFetcher->get('offset'); $limit = $paramFetcher->get('limit'); $places = $this->get('doctrine.orm.entity_manager') ->getRepository('AppBundle:Place') ->findAll(); /* @var $places Place[] */ return $places; } // ... } |
Avec le param fetcher, nous pouvons récupérer nos paramètres et les traiter à notre convenance.
Pour gérer la pagination avec Doctrine, nous pouvons utiliser le query builder avec les paramètres offset
et limit
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?php # src/AppBundle/Controller/PlaceController.php namespace AppBundle\Controller; // ... use FOS\RestBundle\Controller\Annotations\QueryParam; use FOS\RestBundle\Request\ParamFetcher; // ... class PlaceController extends Controller { /** * @Rest\View(serializerGroups={"place"}) * @Rest\Get("/places") * @QueryParam(name="offset", requirements="\d+", default="", description="Index de début de la pagination") * @QueryParam(name="limit", requirements="\d+", default="", description="Nombre d'éléments à afficher") */ public function getPlacesAction(Request $request, ParamFetcher $paramFetcher) { $offset = $paramFetcher->get('offset'); $limit = $paramFetcher->get('limit'); $qb = $this->get('doctrine.orm.entity_manager')->createQueryBuilder(); $qb->select('p') ->from('AppBundle:Place', 'p'); if ($offset != "") { $qb->setFirstResult($offset); } if ($limit != "") { $qb->setMaxResults($limit); } $places = $qb->getQuery()->getResult(); return $places; } // ... } |
Nous pouvons maintenant tester plusieurs appels API :
GET rest-api.local/places?limit=5
permet de lister cinq lieux ;GET rest-api.local/places?offset=3
permet de lister tous les lieux en omettant les trois premiers lieux ;GET rest-api.local/places?offset=1&limit=2
permet de lister deux lieux en omettant le premier lieu dans l’application.
En testant le dernier exemple avec Postman, nous avons :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | [ { "id": 2, "name": "Mont-Saint-Michel", "address": "50170 Le Mont-Saint-Michel", "prices": [], "themes": [ { "id": 3, "name": "history", "value": 3 }, { "id": 4, "name": "art", "value": 7 } ] }, { "id": 4, "name": "Disneyland Paris", "address": "77777 Marne-la-Vallée", "prices": [], "themes": [] } ] |
Trier la liste des lieux
Pour pratiquer, nous allons rajouter un paramètre pour trier les lieux selon leur nom.
Le paramètre s’appellera sort
et pourra avoir deux valeurs: asc pour l’ordre croissant et desc pour l’ordre décroissant.
La valeur par défaut sera null
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <?php # src/AppBundle/Controller/PlaceController.php namespace AppBundle\Controller; // ... use FOS\RestBundle\Controller\Annotations\QueryParam; // ... class PlaceController extends Controller { /** * @Rest\View(serializerGroups={"place"}) * @Rest\Get("/places") * @QueryParam(name="offset", requirements="\d+", default="", description="Index de début de la pagination") * @QueryParam(name="limit", requirements="\d+", default="", description="Nombre d'éléments à afficher") * @QueryParam(name="sort", requirements="(asc|desc)", nullable=true, description="Ordre de tri (basé sur le nom)") */ public function getPlacesAction(Request $request, ParamFetcher $paramFetcher) { $offset = $paramFetcher->get('offset'); $limit = $paramFetcher->get('limit'); $sort = $paramFetcher->get('sort'); $qb = $this->get('doctrine.orm.entity_manager')->createQueryBuilder(); $qb->select('p') ->from('AppBundle:Place', 'p'); if ($offset != "") { $qb->setFirstResult($offset); } if ($limit != "") { $qb->setMaxResults($limit); } if (in_array($sort, ['asc', 'desc'])) { $qb->orderBy('p.name', $sort); } $places = $qb->getQuery()->getResult(); return $places; } // ... } |
La seule différence avec les deux autres query strings est que pour avoir une valeur par défaut à null
, nous utilisons l’attribut nullable
.
En testant l’appel précédant avec en plus un tri des noms par ordre décroissant :
La réponse change en :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | [ { "id": 6, "name": "test", "address": "test", "prices": [], "themes": [] }, { "id": 9, "name": "Musée du Louvre", "address": "799, rue de Rivoli, 75001 Paris", "prices": [ { "id": 6, "type": "less_than_12", "value": 6 }, { "id": 7, "type": "for_all", "value": 15 } ], "themes": [] } ] |
Il est aussi possible de configuer FOSRestBundle pour injecter directement les query strings dans l’objet Request
. Pour plus d’informations,vous pouvez consulter la documentation du bundle.
Les query strings permettent d’étendre facilement une API REST tout en respectant les contraintes que ce style d’architecture nous impose.
Nous venons de brosser une infime partie des fonctionnalités que les query strings peuvent apporter à une API.
D’ailleurs, il n’existe pas de limites réelles et vous pouvez laisser libre cours à votre imagination pour étoffer notre API.
De la même façon, le bundle FOSRestBundle propose un ensemble de fonctionnalité grâce au Param Fetcher Listener qui permettent de gérer les query strings d’une manière assez simple.
La documentation officielle est complète sur le sujet et pourra toujours vous servir de référence.