Quand utiliser les query strings ?

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 ?

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.

The query component contains non-hierarchical data that, along with data in the path component (Section 3.3), serves to identify a resource within the scope of the URI’s scheme and naming authority (if any).

RFC 3986

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 :

Récupération des lieux avec une pagination
 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 :

Récupération des lieux avec une pagination et un tri par ordre décroissant de nom

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.