Il reste un point sur les contraintes REST que nous n’avons toujours pas abordé : l’Hypermédia. En plus, notre API supporte un seul format le JSON. Toutes les requêtes et toutes les réponses sont en JSON. Nous imposons donc une contrainte aux futurs clients de notre API.
Pour remédier à cela, nous allons voir comment supporter facilement d’autre format de réponse en utilisant FOSRestBundle et le sérialiseur de Symfony. Et pour finir, nous verrons comment mettre en place de l’hypermédia dans une API REST, son utilité et comment l’exploiter (si cela est possible) ?
Supporter plusieurs formats de requêtes et de réponses
Cas des requêtes
Depuis que nous avons installé FOSRestBundle, notre API supporte déjà trois formats: le JSON, le format x-www-form-urlencoded (utilisé par les formulaires) et le XML.
Le body listener que nous avons activé utilise déjà par défaut ces trois formats. Pour déclarer le format utilisé dans la requête, il suffit d’utiliser l’entête HTTP Content-Type
qui permet de décrire le type du contenu de la requête (et même de la réponse).
Avec Postman, nous pouvons tester la création d’un utilisateur en exploitant cette fonctionnalité. Au lieu d’avoir du JSON, nous devons juste formater la requête en XML. Le corps de la requête doit être :
1 2 3 4 5 | <user> <firstname>test</firstname> <lastname>XML</lastname> <email>test@xml.fr</email> </user> |
Chaque format a un type MIME qui permet de le décrire avec l’entête Content-Type
:
- JSON: Application/json
- XML: application/xml
C’est au client de définir dans sa requête le format utilisé pour que le serveur puisse la traiter correctement.
Avec Postman, il y a un onglet Headers qui permet de rajouter des entêtes HTTP. Pour faciliter le travail, nous pouvons aussi choisir dans l’onglet Body, le contenu de la requête. Postman rajoutera automatiquement le bon type MIME de la requête à notre place.
En envoyant la requête, l’utilisateur est créé et nous obtenons une réponse en … JSON ! Nous allons donc voir dans la partie suivante comment autoriser plusieurs formats de réponses comme nous l’avons déjà pour les requêtes.
Il est possible de supporter d’autres formats en plus de celle par défaut. Pour en savoir plus, vous pouvez consulter la documentation officielle.
Cas des réponses
L’utilisation de l’annotation View
de FOSRestBundle permet de créer des réponses qui peuvent être affichées dans différents formats. Dans tous nos contrôleurs, nous nous contentons de renvoyer un objet ou un tableau et ces données sont envoyées au client dans le bon format.
Pour supporter plusieurs formats, les données renvoyées par les contrôleurs ne changent pas. Nous devons juste configurer FOSRestBundle correctement. Ce bundle supporte deux types de réponses :
- celles ne nécessitant pas de template pour être affichées : celles au format JSON, au format XML, etc. Il suffit d’avoir les données pour les encoder et le sérialiseur fait le reste du travail.
- celles qui nécessitent un template : le html, etc. Pour ce genre de réponse, nous devons avoir des informations en plus permettant de décorer la réponse (mise en page, CSS, etc.) et le moteur de rendu (ici Twig) s’occupe du reste.
Dans le cadre du cours, nous allons juste aborder le premier type de réponse. La documentation couvre bien l’ensemble du sujet si cela vous intéresse.
Pour activer ces fonctionnalités, nous devons configurer deux sections. La première nous permettra de déclarer les formats de réponses supportés et la seconde nous permettra de configurer la priorité entre ces formats, le comportement du serveur si aucun format n’est choisi par le client, etc.
Nous allons supporter les formats JSON et XML pour les réponses. La configuration devient maintenant (la clé formats
a été rajoutée) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 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'], fallback_format: 'json', prefer_extension: false } body_listener: enabled: true |
En réalité, ces deux formats sont déjà activés par défaut mais par soucis de clarté nous allons les laisser visibles dans le fichier de configuration.
Le reste de la configuration se fait avec la clé rules
. C’est au niveau des priorités (clé priorities
) que les formats supportés sont définis. Pour notre configuration, nous avons une seule règle. Mais il est tout à fait possible de définir plusieurs règles différentes selon les URL utilisées. Nous pouvons imaginer par exemple une règle par version de l’api, ou bien encore une règle par ressources.
Il suffit de rajouter le format XML aux priorités et notre API pourra répondre aussi bien en XML qu’en JSON.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 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 ` |
C’est maintenant au client d’informer le serveur sur le ou les formats qu’il préfère.
L’ordre de déclaration est très important ici. Si une requête ne spécifie aucun format alors le serveur choisira du JSON.
La négociation de contenu
La négociation de contenu est un mécanisme du protocole HTTP qui permet de proposer plusieurs formats pour une même ressource.
Pour sa mise en œuvre, le client doit envoyer un entête HTTP de la famille Accept
. Nous avons entre autres :
Entête | Utilisation |
---|---|
Accept | Pour choisir un média type (text, json, html etc). |
Accept-Charset | Pour choisir le jeu de caractères (iso-8859-1, utf8, etc.) |
Accept-Language | Pour choisir le langage (français, anglais, etc.) |
L’entête qui nous intéresse ici est Accept
. Comme pour l’entête Content-Type
, la valeur de cet entête doit contenir un type MIME.
Mais en plus, avec cet entête, nous pouvons déclarer plusieurs formats à la fois en prenant le soin de définir un ordre de préférence en utilisant un facteur de qualité.
Le facteur de qualité (q
) est un nombre compris entre 0 et 1 qui permet de définir l’ordre de préférence. Plus q
est élevé, plus le type MIME associé est prioritaire.
Une requête avec comme entête Accept: application/json;q=0.7, application/xml;q=1,
veut dire que le client préfère du XML et en cas d’indisponibilité du XML alors du JSON.
Une requête avec comme entête Accept: application/xml
veut dire que le client préfère du XML. Si le facteur de qualité n’est pas spécifié, sa valeur est à 1
.
Pour tester, nous allons ajouter cet entête à une requête pour lister tous les lieux de notre API.
La réponse est bien en XML et nous pouvons tester avec n’importe quelle méthode de notre API.
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 49 50 51 52 53 54 55 56 57 58 59 60 | <?xml version="1.0"?> <response> <item key="0"> <id>1</id> <name>Tour Eiffel</name> <address>5 Avenue Anatole France, 75007 Paris</address> <prices> <id>1</id> <type>less_than_12</type> <value>5.75</value> </prices> <themes> <id>1</id> <name>architecture</name> <value>7</value> </themes> <themes> <id>2</id> <name>history</name> <value>6</value> </themes> </item> <item key="1"> <id>2</id> <name>Mont-Saint-Michel</name> <address>50170 Le Mont-Saint-Michel</address> <prices/> <themes> <id>3</id> <name>history</name> <value>3</value> </themes> <themes> <id>4</id> <name>art</name> <value>7</value> </themes> </item> <item key="2"> <id>4</id> <name>Disneyland Paris</name> <address>77777 Marne-la-Vallée</address> <prices/> <themes/> </item> <item key="3"> <id>5</id> <name>Aquaboulevard</name> <address>4-6 Rue Louis Armand, 75015 Paris</address> <prices/> <themes/> </item> <item key="4"> <id>6</id> <name>test</name> <address>test</address> <prices/> <themes/> </item> </response> |
Le serveur renvoie aussi un entête Content-Type
pour signaler au client le format de la réponse.
Attention, certaines API proposent de rajouter un format à une URL pour sélectionner un format de réponse (places.json, places.xml, etc.). Cette technique ne respecte pas les contraintes REST vu que l’URL doit juste servir à identifier une ressource.
L'Hypermédia
La dernière contrainte du REST que nous n’avons pas encore implémentée est l’hypermédia en tant que moteur de l’état de l’application HATEOAS. Pour rappel, le contrôle hypermédia désigne l’état d’une application ou API avec un seul point d’entrée mais qui propose des éléments permettant de l’explorer et d’interagir avec elle.
Avec un humain qui surfe sur le web, il est facile de suivre cette contrainte. En général, nous utilisons tous des sites web en tapant sur notre navigateur l’URL de la page d’accueil. Ensuite, avec les différents liens et formulaires, nous interagissons avec ledit site. Un site web est l’exemple parfait du concept HATEAOS.
Pour une API, nous avons des outils comme BazingaHateoasBundle qui permettent d’avoir un semblant de HATEOS.
Une fois configuré, voici un exemple de réponse lorsqu’on récupère un utilisateur (exemple issu de la documentation du bundle).
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 | { "id": 42, "first_name": "Adrien", "last_name": "Brault", "_links": { "self": { "href": "/api/users/42" }, "manager": { "href": "/api/users/23" } }, "_embedded": { "manager": { "id": 23, "first_name": "Will", "last_name": "Durand", "_links": { "self": { "href": "/api/users/23" } } } } } |
Les attributs _links
et _embedded
sont issus des spécifications Hypertext Application Language (HAL). Ils permettent de décrire notre ressource en suivant les spécifications HAL encore à l’état de brouillon.
Des initiatives identiques comme JSON for Linking Data (json-ld) tentent de traiter le problème mais se heurtent tous face à un même obstacle.
La contrainte HATEOAS de REST nécessite un client très intelligent qui puisse :
- comprendre les relations déclarées entre ressource ;
- auto-découvrir notre API à partir d’une seule URL.
Malheureusement, il n’existe pas encore de client d’API en mesure de comprendre et d’exploiter une API RESTFul niveau 3 (selon le modèle de Richardson).
Nous n’implémenterons donc pas cette contrainte et c’est le cas pour beaucoup d’API REST. Dans les faits, cela ne pose aucun problème et notre API est pleinement fonctionnelle.
Le support de plusieurs formats de requêtes et de réponses se fait en utilisant la négociation de contenu.
Les entêtes mis en œuvre pour atteindre un tel comportement sont Accept
et Content-Type
. FOSRestBundle exploite ensuite les capacités de notre sérialiseur afin de produire des réponses pour différents formats en se basant sur les mêmes données.