Creez Une API
Creez Une API
12 août 2019
Table des matières
1
Table des matières
2
Table des matières
3
Table des matières
4
Table des matières
5
Première partie
6
I. Un tour d’horizon des concepts REST
Nous allons explorer les origines de REST et l’ensemble des concepts et contraintes qui sont
autour.
?
Pourquoi utiliser REST plutôt qu’une autre technologie ou architecture ? Quelles avantages
cela peut-il nous apporter ?
7
1. REST en quelques mots
Le style d’architecture REST représente un ensemble de contraintes qui régissent une application
réseau. Chacune de ces contraintes décrit un concept qu’une application qui se veut RESTful
doit implémenter.
i
Le terme RESTful (anglicisme) est un adjectif qui qualifie une application qui suit les
principes REST.
8
I. Un tour d’horizon des concepts REST
Une autre contrainte est la notion de ”Sans état” ou Stateless en anglais. La communication
entre le client et le serveur doit se faire sans dépendre d’un contexte lié au serveur. Chaque
requête du client contient toutes les informations nécessaires à son traitement. Ainsi, plusieurs
instances de serveurs peuvent traiter indifféremment les requêtes de chaque client.
1.2.1.3. Cache
Afin d’améliorer les performances de l’application, le serveur doit ajouter une étiquette de cache
à toutes les réponses. Cette étiquette décrit les possibilités de mise en cache ou non des données
renvoyées par le serveur.
Une des fonctionnalités clés qui permet de distinguer une architecture REST est la mise en
valeur d’une interface uniforme entre les différents composants.
REST repose sur 4 contraintes d’interface :
— l’identification de manière unique des ressources ;
— l’interaction avec les ressources via des représentations, chaque ressource disposant de sa
présentation ;
— les messages auto-descriptifs, une réponse ou une requête contient toutes les informations
permettant de décrire la nature des données qu’elle contient et les interactions possibles ;
— et, l’hypermédia en tant que moteur de l’état de l’application HATEOAS. L’état de
l’application, les différentes interactions possibles entre client et le serveur doivent être
décrites à travers les liens hypermédia dans les réponses du serveur.
Le terme lien hypermédia englobe des formulaires, les liens hypertextes ou plus généralement
tout support numérique permettant une interaction.
En définissant une interface uniformisée, les différentes interactions avec le serveur sont facilement
identifiables.
Les couches dans une application consistent en l’isolation des différents composants de l’applica-
tion pour bien organiser leurs responsabilités. Chaque couche représente alors un système borné
qui traite une problématique spécifique de notre application. Nous pouvons prendre comme
exemple une couche dédiée au stockage des données mais qui n’a pas conscience de leur origine.
Son unique rôle consiste à stocker des informations qui lui sont passées.
9
I. Un tour d’horizon des concepts REST
Ce style d’architecture introduit et utilise par la même occasion quelques notions qu’il faut
impérativement comprendre.
Une interface REST gravite autour de ressources. À partir du moment où vous devez interagir
avec une entité de votre application, créer une entité, la modifier, la consulter ou encore
l’identifier de manière unique, vous avez pour la plupart des cas une ressource. Si par exemple,
nous développons une application de commerce en ligne, un article disponible à la vente est une
ressource. Une image décrivant cet article peut être une ressource. Pour référencer de manière
unique cette ressource, REST utilise un identifiant. Cet identifiant sera alors utilisé par tous les
différents composants de notre application afin d’interagir avec cette ressource. Notre article
pourra ainsi avoir un numéro unique que les composants du panier, de paiement ou autres
utiliseront pour désigner cet article.
Une représentation désigne toutes les informations (données et métadonnées) utiles qui décrivent
une ressource.
Notre article pourra donc être représenté par une page HTML (Hypertext Markup langage)
contenant le nom de l’article, son prix, sa disponibilité etc. Et notre image décrivant un article,
sa représentation désignera simplement les données en base64 et les métadonnées qui décrivent
l’encodage utilisée pour l’image, le type de compression, etc.
i
En résumé, REST est un style d’architecture défini par un ensemble de contraintes qui
régissent l’organisation d’une application et les procédés de communication entre un
fournisseur de services (le serveur) et le consommateur (le client).
10
2. Pourquoi utiliser REST
?
Pourquoi utiliser REST plutôt d’une autre technologie ou architecture ?
Il existe plusieurs moyens permettant de communiquer entre des composants dans le cadre d’une
architecture de type SOA (Service oriented Archictecture). On peut citer le protocole SOAP
(Simple Object Access Protocol) ou encore XML-RPC.
Ces technologies sont largement utilisées surtout dans un cadre d’entreprise, mais avec l’essor
du web, elles ont commencé à montrer leurs limites.
REST étant conçu pour répondre à ce besoin spécifique - le web - ce style d’architecture théorisé
par Roy Fielding présente intrinsèquement beaucoup d’avantages pour ce cas d’usage.
Dans cette partie de ce cours, nous allons donc voir les facilités et l’intérêt que REST pourrait
nous apporter dans le cadre du développement d’une API web.
Comme les différentes règles et design pattern appliqués en génie logiciel, les différentes
contraintes qu’impose l’architecture REST permettent d’obtenir des applications de meilleure
qualité.
On peut citer entre autres :
— un couplage plus faible entre le client et le serveur comparé aux méthodes du type RPC
Remote Procedure Call comme SOAP ;
— une uniformisation des APIs (Application Programming Interface) pour une facilité
d’utilisation ;
— une plus grande tolérance à la panne ;
— ou encore une application facilement portable et extensible.
11
I. Un tour d’horizon des concepts REST
2.1.2. Popularisation
Bien que la publication de la thèse de Roy Fielding date des années 2000, un livre de Leonard
Richardson et Sam Ruby RESTful Web Services, sorti le 22 mai 2007, a popularisé le style
d’architecture REST en proposant une méthodologie pour l’implémenter en utilisant le protocole
HTTP Hypertext Transfert Protocol.
Comme vous l’aurez déjà remarqué, plus nous avançons dans les principes REST, plus le modèle
devient contraignant. Dès lors, une application peut suivre ces principes sans pour autant remplir
toutes les contraintes du REST.
Ainsi, lors de la conférence QCon du 20 novembre 2008 , Richardson a présenté un modèle
qui permet d’évaluer son application selon les principes REST. Ce modèle est connu sous le
nom de : Modèle de maturité de Richardson.
Le protocole HTTP est utilisé pour appeler des méthodes du serveur. C’est le niveau des API
Json RPC ou encore SOAP.
Les entités avec lesquels les interactions ont lieu sont identifiées en tant que ressources.
Les interactions avec le serveur se font avec plusieurs verbes HTTP différents en respectant
leurs sémantiques. Les opérations avec une ressource se font via un même identifiant mais avec
des verbes différents.
Par exemple, le verbe GET pour récupérer du contenu ou DELETE pour le supprimer. En
l’occurrence, le Json RPC utilise le verbe POST pour toutes ces opérations et par conséquent
ne respecte pas ce modèle.
i
Les verbes HTTP appelés aussi méthodes permettent de décrire avec une sémantique
claire l’opération que nous voulons effectuer. Nous pouvons citer les plus courantes qui
sont GET, POST, PUT et DELETE.
Pour finir les codes de statut du protocole permettent d’avoir des réponses plus expressives. Une
réponse avec un code 404 permettra au client d’identifier que la ressource demandée n’existe
pas. Nous verrons plus en détails quelles sont les méthodes et codes de statut que nous pouvons
utiliser dans la suite de ce cours.
12
I. Un tour d’horizon des concepts REST
Comme déjà décrit dans la partie Présentation de REST > Interface uniforme, 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. Un bon exemple est le site web. Si
par exemple, nous accédons à YouTube, la page d’accueil nous propose des liens vers des vidéos
ou encore un formulaire de recherche. Ces éléments hypermédia permettent ainsi de visualiser
toutes sortes de contenus sans connaitre au préalable les liens directs les identifiants.
http://zestedesavoir.com/media/galleries/3183/
Notre objectif sera de se rapprocher le plus possible de l’architecture REST sans oublier les
contraintes que le monde réel va nous imposer.
As described in Chapter 4, the motivation for developing REST was to create an architectural
model for how the Web should work, such that it could serve as the guiding framework for the
Web protocol standards. REST has been applied to describe the desired Web architecture,
help identify existing problems, compare alternative solutions, and ensure that protocol
extensions would not violate the core constraints that make the Web successful.
Le protocole de transfert HTTP dispose de beaucoup de spécificités que nous pouvons donc
mettre en oeuvre avec le style d’architecture REST. Nous verrons comment mettre à profit ces
spécifications afin de remplir les exigences d’une application dite RESTful.
L’essence même du HTTP - protocole de transfert hypertexte - comme son nom l’indique est de
permettre le transfert de données entre un client et un serveur. Dès lors, les applications web
remplissent de-facto cette contrainte d’architecture.
L’utilisation de HTTP dans le cadre de REST pour une bonne isolation client-serveur est donc
un choix judicieux et très largement répandu.
13
I. Un tour d’horizon des concepts REST
The Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol for distri-
buted, collaborative, hypertext information systems.
Le protocole de transfert hypertexte (HTTP) est un protocol sans état de la couche applica-
tion (se référer au modèle OSI) pour les systèmes d’informations hypermédia distribuées et
collaboratifs.
Le protocole HTTP est stateless (sans état) par définition. Même si nous pouvons retrouver des
applications web qui dérogent à cette contrainte, il faut retenir que HTTP a été pensé pour
fonctionner sans état.
2.2.3. Le Cache
Là aussi, le protocole HTTP supporte nativement la gestion du cache via les entêtes comme
Cache-Control, Expires, etc. Ces entêtes permettent de réutiliser une même réponse si le contenu
est considéré comme étant à jour comme le préconise le style d’architecture REST afin d’améliorer
les performances de notre application.
2.2.4.1. Identification
Nous avons déjà défini une ressource dans le cadre de REST et pourquoi il fallait l’identifier de
manière unique. Le protocole HTTP utilise là aussi une notion bien connue : l’URI (Uniform
Resource Identifier). En effet, lorsque nous consultons la RFC 2731 de HTTP 1.1, nous
pouvons voir que une ressource est définie comme étant :
The target of an HTTP request is called a ”resource”. HTTP does not limit the nature of a
resource ; it merely defines an interface that might be used to interact with resources. Each
resource is identified by a Uniform Resource Identifier (URI), as described in Section 2.7 of
[RFC7230].
La cible d’une requête HTTP est appelé une « ressource ». HTTP ne met pas de limitation sur
la nature d’une ressource ; il définit seulement une interface qui peut être utilisé pour interagir
avec des ressources. Chacune de ces ressources est identifiée par une URI (Uniform Resource
Identifier), comme décrit dans la section 2.7 de la [RFC7230].
2.2.4.2. Représentation
Une représentation est toute information destinée à refléter l’état passé, actuel ou voulu d’une
ressource donnée.
14
I. Un tour d’horizon des concepts REST
RFC 7231
Ainsi avec les URI et les représentations des réponses HTTP (html, xml, json, etc.), nous
pouvons satisfaire la contrainte 4 d’interface uniforme de REST pour mettre en place notre
application.
2.3. Ce cours
Durant ce cours, nous allons développer une API permettant de gérer des idées et suggestions
de sorties récréatives en se basant sur les concepts REST. Cette application va nous servir de fil
conducteur pour ce cours et toutes ses fonctionnalités seront détailllées plus tard.
Les prérequis pour suivre ce cours, il faut des connaissances minimum de Symfony 2.7 à 3.* :
— créer une application avec Symfony ;
— Utiliser Doctrine 2 avec Symfony ;
— Utiliser l’injection de dépendances de Symfony.
Les objectifs de ce cours sont entre autres de :
— Comprendre l’architecture REST ;
— Mettre en place une API RESTful (Créer une API uniforme et facile à utiliser) ;
— Apprendre comment sécuriser une API (REST en particulier) ;
— Savoir utiliser les avantages de Symfony dans ses développements (Composants et
Bundles).
Nous allons mettre en place une application permettant de gérer des idées et suggestions de
sorties récréatives. L’application dispose de plusieurs lieux (restaurants, centre de loisirs, cinéma
etc) connus et réputés et de plusieurs utilisateurs avec leurs centres d’intérêt. L’objectif est de
proposer un mécanisme permettant de proposer à chaque utilisateur une idée de sortie la plus
pertinente en se basant sur ses préférences.
Les exemples présentés se baseront sur Symfony 3 avec FOSRestBundle. Les tests de l’API
se feront avec cURL (utilitaire en ligne de commande) et le logiciel Postman (extension du
navigateur Chrome).
15
I. Un tour d’horizon des concepts REST
Le protocole HTTP se prête bien au jeu de REST. À l’heure actuelle, la plupart des API
RESTful l’utilisent vu que les technologies pour l’exploiter sont très largement répandues.
Ici prend fin l’aparté sur la partie théorique de ce cours. La suite sera grandement axée sur la
pratique, tous les concepts seront abordés en se basant sur des exemples concrets.
Nous allons donc voir comment appliquer les concepts et contraintes REST dans une application
web. Cela nous offrira une API uniforme avec une prise en main facile. L’objectif est d’avoir à la
fin de ce cours une API pleinement fonctionnelle.
16
Deuxième partie
17
3. Notre environnement de développement
Afin d’avoir un environnement de développement de référence pendant ce cours, nous allons
voir ensemble les technologies qui seront utilisées et surtout à quelles versions.
Vous pourrez ainsi tester les différents codes qui seront fournis. Il est utile de rappeler que pour
suivre ce cours vous devez avoir un minimum de connaissances en PHP et Symfony. Certains
aspects de configuration comme l’installation de MySQL, Apache ou autres ne seront pas abordés.
Si vous n’avez jamais procédé à l’installation de Symfony, il est préférable de se documenter sur
le sujet avant de commencer ce cours.
3.1.1. Plateforme
Nous avons ci-dessous un tableau récapitulatif des différentes technologies et la version utilisée.
Le système d’exploitation utilisé importe peu et vous pouvez donc utiliser celui de votre choix
pour suivre le reste du cours. Sur Windows, vous avez la suite WAMP et son équivalent
LAMP sur Ubuntu.
La méthode recommandée pour installer un projet Symfony est d’utiliser l’installateur . Cet
utilitaire nous permettra d’installer Symfony avec la version que nous souhaitons.
18
II. Développement de l’API REST
Avant tout, il faut s’assurer que l’exécutable de PHP est bien disponible dans l’invite de
commande. Des consignes d’installation sont disponibles sur le site officiel de PHP . Ensuite,
Il suffit exécuter dans l’invite de commande :
i
Il est possible de placer les fichiers symfony et symfony.bat dans un même dossier que
vous rajouter dans le PATH avec les variables d’environnement de Windows afin d’accéder
à la commande partout.
Une fois l’installation finie, lancer la commande symfony pour vérifier le bon fonctionnement du
tout.
symfony
# réponse attendue
Symfony Installer (1.5.0)
=========================
3.1.3. Composer
19
II. Développement de l’API REST
composer
# réponse attendue
# ______
# / ____/___ ____ ___ ____ ____ ________ _____
# / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
#/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /
#\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
# /_/
Si vous n’avez pas déjà Git sur votre machine, il va falloir l’installer car Composer peut être
amené à l’utiliser pour télécharger des dépendances. L’installation est bien détaillée sur le site
Git SCM . Il faudra juste vous assurer que l’exécutable git est disponible dans votre path.
Maintenant que nous avons un environnement de développement bien configuré, il ne reste plus
qu’à créer un nouveau projet basé sur Symfony 3.
Après un long moment de chargement, nous avons un dossier nommé rest_api contenant une
installation toute neuve de Symfony 3.1.X (3.1.1 pour mon cas). Ça sera notre point de départ
pour nos développements. Testons la création du projet en lançant le serveur de développement
intégré à PHP :
cd rest_api
php bin/console server:run
20
II. Développement de l’API REST
Accédez à l’URL de vérification de Symfony et effectuez les correctifs si nécessaires http ://lo-
calhost :8000/config.php .
http://zestedesavoir.com/media/galleries/3183/
[GuzzleHttp\Exception\RequestException]
cURL error 60: SSL certificate problem: unable to get local issuer
certificate
Pour corriger ce problème, il faut s’assurer que l’extension OpenSSL est activée et définir le
chemin d’accès vers le fichier contenant les certificats racines.
Une liste de certificats est disponible sur https ://curl.haxx.se/ca/cacert.pem . Pensez à le
télécharger.
Commençons par identifier le fichier de configuration de PHP. Avec WAMP, ce fichier se situe
dans le dossier d’installation (par exemple, D:\wamp64\bin\php\php7.0.0\php.ini). Il suffit
maintenant de vérifier que la ligne concernant l’extension OpenSSL n’est pas commenté et de
spécifier le chemin du fichier contenant les certificats racines.
extension=php_openssl.dll
[openssl]
openssl.cafile=D:\wamp64\bin\php\php7.0.0\cacert.pem
;openssl.capath=
Durant le reste du cours, j’accéderai à l’API en utilisant un virtual host apache personnalisé.
Notre API sera donc disponible sur l’URL http://rest-api.local.
Pour ce faire, il faut configurer un virtual host apache et modifier le fichier host du système
pour renseigner l’URL rest-api.local.
21
II. Développement de l’API REST
i
Le virtual host fourni est compatible avec Windows. Penser à remplacer
D:/wamp64/www/rest_api par votre dossier d’installation et à effectuer les adaptations
nécessaires pour un autre système d’exploitation.
<VirtualHost *:80>
ServerName rest-api.local
DocumentRoot "D:/wamp64/www/rest_api/web"
<Directory "D:/wamp64/www/rest_api/web">
DirectoryIndex app_dev.php
Require all granted
AllowOverride None
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^ - [L]
RewriteRule ^ app_dev.php [L]
</Directory>
!
Le mode rewrite d’apache est obligatoire pour que ce virtual host fonctionne. Notez aussi
que les requêtes seront redirigées directement vers app_dev.php avec cette configuration.
127.0.0.1 rest-api.local
::1 rest-api.local
Sous Windows, l’astuce consiste à lancer votre éditeur de texte en tant qu’administrateur avant
d’ouvrir le fichier à éditer.
Maintenant en accédant à l’URL http ://rest-api.local/ , nous atteignons notre page web de
bienvenue.
22
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Maintenant que nous avons un environnement de développement fonctionnel, nous allons mettre
en place toutes les briques nécessaires pour avoir une API REST complète. Les choses sérieuses
peuvent maintenant commencer. L’outil Postman sera utilisé pour effectuer tous les tests de
notre API. Il est donc grandement recommandé de l’installer avant de continuer.
23
4. Premières interactions avec les
ressources
?
Pourquoi parle-t-on tant des ressources ?
Au-delà de connaitre la définition d’une ressource en REST, un des principaux problèmes lorsque
nous développons une API est de savoir quelles sont les entités de notre projet qui sont éligibles.
Dans cette partie du cours, nous allons donc voir comment bien identifier une ressource avec une
expression du besoin plus claire et aussi comment les exposer avec une URI (Uniform Resource
Identifier).
Une interface REST gravite autour de ressources. À partir du moment où vous devez interagir
avec une entité de votre application, créer une entité, la modifier, la consulter ou encore
l’identifier de manière unique, vous avez pour la plupart des cas une ressource.
L’application que nous devons développer enregistre plusieurs lieux (monuments, centres de
loisirs, châteaux, etc.) et fait des suggestions de sorties/visites à des utilisateurs.
Dans notre application, nous aurons donc un lieu avec éventuellement les informations permettant
de le décrire (nom, adresse, thème, réputation, etc.).
Nous serons surement appelés à le consulter ou à l’éditer. Voici donc notre première ressource :
un lieu.
Le choix des ressources dans une API REST est très important mais leur nommage l’est autant
car c’est cela qui permettra d’avoir une API cohérente.
A ce stade du cours, la notion de ressource doit être bien comprise. Mais il existe aussi une
autre notion qui sera utile dans la conception d’une API REST : les collections.
Une collection désigne simplement un ensemble de ressources d’un même type. Dans notre cas,
la liste de tous les lieux référencés dans l’application représente une collection. Et c’est idem
pour la liste des utilisateurs.
24
II. Développement de l’API REST
Une règle d’or à respecter, c’est la cohérence. Il faut choisir des noms de ressources simples et
suivre une même logique de nommage. Si par exemple, une ressource est nommée au pluriel
alors elle doit l’être sur toute l’API et toutes les ressources doivent être aussi au pluriel. La
casse est également très importante pour la cohérence. Il faudra ainsi respecter la même casse
pour toutes les ressources.
Pour le cas de notre exemple, toutes nos ressources seront en minuscule, au pluriel et en anglais.
C’est un choix assez répandu dans les différentes API publiques à notre disposition.
Donc pour une collection représentant les lieux à visiter, nous aurons places. Dans notre URL,
nous aurons alors rest-api.local/places.
Pour commencer, nous considérons qu’un lieu a un nom et une adresse. L’objectif est d’avoir un
appel de notre API permettant d’afficher tous les lieux connus par notre application.
La ressource avec laquelle nous souhaitons interagir est places. Notre requête HTTP doit donc
se faire sur l’URL rest-api.local/places.
?
Quelle méthode (ou verbe) HTTP utiliser : GET, POST, ou DELETE ?
Comme expliqué dans le modèle de maturité de Ridcharson, une API qui se veut RESTful doit
utiliser les méthodes HTTP à bon escient pour interagir avec les ressources. Dans notre cas, nous
voulons lire des données disponibles sur le serveur. Le protocole HTTP nous propose la méthode
GET qui, selon la RFC 7231 , est la méthode de base pour récupérer des informations.
http://zestedesavoir.com/media/galleries/3183/
4.1.4.2. Implémentation
Nous allons commencer par mettre en place notre appel API avec de fausses données, ensuite
nous mettrons en place la persistance de celles-ci avec Doctrine.
Tout d’abord, il faut créer une entité nommée Place contenant un nom et une adresse :
25
II. Développement de l’API REST
# src/AppBundle/Entity/Place.php
<?php
namespace AppBundle\Entity;
class Place
{
public $name;
public $address;
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Place;
26
II. Développement de l’API REST
}
}
Un appel de type GET sur l’URL rest-api.local/places permet d’obtenir notre liste de lieux.
[
{
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris"
},
{
"name": "Mont-Saint-Michel",
"address": "50170 Le Mont-Saint-Michel"
},
{
"name": "Château de Versailles",
"address": "Place d'Armes, 78000 Versailles"
}
]
Avec Postman :
http://zestedesavoir.com/media/galleries/3183/
Nous allons maintenant récupérer nos lieux depuis la base de données avec Doctrine. Rajoutons
un identifiant aux lieux et mettons en place les annotations sur l’entité Place.
# src/AppBundle/Entity/Place.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="places")
*/
class Place
{
/**
27
II. Développement de l’API REST
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="string")
*/
protected $address;
28
II. Développement de l’API REST
Pour des raisons de clarté, nous allons aussi modifier le nom de notre base de données.
# app/config/parameters.yml
parameters:
database_host: 127.0.0.1
database_port: null
database_name: rest_api
database_user: root
database_password: null
Il ne reste plus qu’à créer la base de données et la table pour stocker les lieux.
Nous disposons maintenant d’une base de données pour gérer les informations de l’application.
Il ne reste plus qu’à changer l’implémentation dans notre contrôleur pour charger les données
avec Doctrine.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
29
II. Développement de l’API REST
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Place;
$formatted = [];
foreach ($places as $place) {
$formatted[] = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
}
[
{
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris"
},
{
"id": 2,
"name": "Mont-Saint-Michel",
"address": "50170 Le Mont-Saint-Michel"
},
{
"id": 3,
"name": "Château de Versailles",
"address": "Place d'Armes, 78000 Versailles"
}
30
II. Développement de l’API REST
Avec Postman :
http://zestedesavoir.com/media/galleries/3183/
4.1.5.1. Objectif
Maintenant que le principe pour récupérer les informations d’une liste est expliqué, nous allons
faire de même avec les utilisateurs. Nous considérerons que les utilisateurs ont un nom, un
prénom et une adresse mail et que la ressource pour désigner une liste d’utilisateur est users.
L’objectif est de mettre en place un appel permettant de générer la liste des utilisateurs
enregistrés en base. Voici le format de la réponse attendue :
[
{
"id": 1,
"firstname": "Ab",
"lastname": "Cde",
"email": "[email protected]"
},
{
"id": 2,
"firstname": "Ef",
"lastname": "Ghi",
"email": "[email protected]"
}
]
4.1.5.2. Implémentation
4.1.5.2.1. Configuration de doctrine Comme pour les lieux, nous allons commencer par créer
l’entité User et la configuration doctrine qui va avec :
31
II. Développement de l’API REST
# src/AppBundle/Entity/User.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="users")
*/
class User
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $firstname;
/**
* @ORM\Column(type="string")
*/
protected $lastname;
/**
* @ORM\Column(type="string")
*/
protected $email;
32
II. Développement de l’API REST
33
II. Développement de l’API REST
4.1.5.2.2. Création du contrôleur pour les utilisateurs Nous allons créer un contrôleur dédié
aux utilisateurs. Pour l’instant, nous aurons une seule méthode permettant de les lister.
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\User;
$formatted = [];
foreach ($users as $user) {
$formatted[] = [
'id' => $user->getId(),
'firstname' => $user->getFirstname(),
'lastname' => $user->getLastname(),
'email' => $user->getEmail(),
];
}
En regardant le code, nous pouvons déjà remarqué que le contrôleur UserController ressemble
à quelques lignes prés au contrôleur PlaceController. Vu qu’avec REST nous utilisons une
interface uniforme pour interagir avec nos ressources, si l’opération que nous voulons effectuer
est identique, il y a de forte chance que le code pour l’implémentation le soit aussi. Cela nous
permettra donc de gagner du temps dans les développements.
En testant avec Postman :
34
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
?
Maintenant que nous savons comment accéder à un ensemble de ressource (une collection),
comment faire pour récupérer un seul lieu ?
D’un point de vue sémantique HTTP, nous savons que pour lire du contenu, il faut utiliser
la méthode GET. Le problème maintenant est de savoir comment identifier la ressource parmi
toutes celles dans la collection.
Le point de départ est, en général, le nom de la collection (places pour notre cas). Nous devons
donc trouver un moyen permettant d’identifier de manière unique un élément de cette collection.
Il a une relation entre la collection et chacune de ses ressources.
Pour le cas des lieux, nous pouvons choisir l’identifiant auto-incrémenté pour désigner de manière
unique un lieu. Nous pourrons dire alors que l’identifiant 1 désigne la ressource Tour Eiffel.
Pour la représenter dans une URL, nous avons deux choix :
— rest-api.local/places ?id=1
— rest-api.local/places/1
On pourrait être tenté par la première méthode utilisant le query string id. Mais la RFC 3986
spécifie clairement les query strings comme étant des composants qui contiennent des données
non-hiérarchiques. Pour notre cas, il y a une relation hiérarchique claire entre une collection et
une de ses ressources. Donc cette méthode est à proscrire.
Notre URL pour désigner un seul lieu sera alors rest-api.local/places/1. Et pour généra-
liser, pour accéder à un lieu, on aura rest-api.local/places/{place_id} où {place_id}
désigne l’identifiant de notre lieu.
4.2.1.2. Implémentation
Mettons maintenant en œuvre un nouvel appel permettant de récupérer un lieu. Nous allons
utiliser le contrôleur PlaceController.
35
II. Développement de l’API REST
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Place;
// code de getPlacesAction
/**
* @Route("/places/{place_id}", name="places_one")
* @Method({"GET"})
*/
public function getPlaceAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('place_id'));
/* @var $place Place */
$formatted = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
Cette action est particulièrement simple et se passe de commentaires. Ce qu’il faut retenir c’est
que la méthode renvoie une seule entité et pas une liste.
En testant, nous avons comme réponse :
{
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris"
}
36
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Nous pouvons rendre la configuration de la route plus stricte en utilisant l’attribut require
ments de l’annotation Route. Puisque les identifiants des lieux sont des entiers, la déclaration
de la route pourrait être @Route("/places/{place_id}", requirements={"place_id" =
"\d+"}, name="places_one").
Bis repetita, nous allons mettre en place une méthode permettant de récupérer les informations
d’un seul utilisateur.
Comme pour les lieux, pour récupérer un utilisateur, il suffit de créer un nouvel appel GET sur
l’URL rest-api.local/users/{id} où {id} désigne l’identifiant de l’utilisateur.
Pour cela, éditons le contrôleur UserController pour rajouter cette méthode.
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\User;
// code de getUsersAction
/**
* @Route("/users/{id}", name="users_one")
* @Method({"GET"})
*/
public function getUserAction(Request $request)
{
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->find($request->get('id'));
/* @var $user User */
37
II. Développement de l’API REST
$formatted = [
'id' => $user->getId(),
'firstname' => $user->getFirstname(),
'lastname' => $user->getLastname(),
'email' => $user->getEmail(),
];
{
"id": 1,
"firstname": "Ab",
"lastname": "Cde",
"email": "[email protected]"
}
http://zestedesavoir.com/media/galleries/3183/
4.3. Les codes de statut (status code) pour des messages plus
expressifs
38
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Ce comportement ne respecte pas la sémantique HTTP. En effet dans n’importe quel site, si
vous essayez d’accéder à une page inexistante, vous recevez la fameuse erreur 404 Not Found
qui signifie que la ressource n’existe pas. Pour que notre API soit le plus RESTful possible, nous
devons implémenter un comportement similaire.
Nous ne devons avoir une erreur 500 que dans le cas d’une erreur interne du serveur. Par
exemple, s’il est impossible de se connecter à la base de données, il est légitime de renvoyer une
erreur 500.
De la même façon, lorsque la ressource est trouvée, nous devons renvoyer un code 200 pour
signifier que tout s’est bien passé. Par chance, ce code est le code par défaut lorsqu’on utilise
l’objet JsonResponse de Symfony. Nous avons donc déjà ce comportement en place.
http://zestedesavoir.com/media/galleries/3183/
Pour notre cas, il est facile de gérer ce type d’erreurs. Nous devons juste vérifier que la réponse du
repository n’est pas nulle. Au cas contraire, il faudra renvoyer une erreur 404 avec éventuellement
un message détaillant le problème.
Pour un lieu, nous aurons donc :
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\Place;
39
II. Développement de l’API REST
/**
* @Route("/places/{place_id}", name="places_one")
* @Method({"GET"})
*/
public function getPlaceAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('place_id'));
/* @var $place Place */
if (empty($place)) {
return new JsonResponse(['message' =>
'Place not found'], Response::HTTP_NOT_FOUND);
}
$formatted = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
Maintenant, une requête GET sur l’URL rest-api.local/places/42 nous renvoie une erreur 404
avec un message bien formaté en JSON. La constante Response::HTTP_NOT_FOUND vaut 404
et est une constante propre à Symfony.
La réponse contient un message en JSON :
{
"message": "Place not found"
}
http://zestedesavoir.com/media/galleries/3183/
40
II. Développement de l’API REST
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\User;
/**
* @Route("/users/{id}", name="users_one")
* @Method({"GET"})
*/
public function getUserAction(Request $request)
{
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->find($request->get('id'));
/* @var $user User */
if (empty($user)) {
return new JsonResponse(['message' =>
'User not found'], Response::HTTP_NOT_FOUND);
}
$formatted = [
'id' => $user->getId(),
'firstname' => $user->getFirstname(),
'lastname' => $user->getLastname(),
'email' => $user->getEmail(),
];
Avec ces modifications, nous avons maintenant une gestion des erreurs propres et l’API respecte
au mieux la sémantique HTTP.
41
II. Développement de l’API REST
Après cette première introduction, nous pouvons retenir qu’en REST les interactions ont lieu
avec soit une collection soit une instance de celle-ci : une ressource.
Chaque opération peut alors être décrite comme étant une requête sur une URL bien identifiée
avec un verbe HTTP adéquat. Le type de la réponse est décrit par un code de statut.
Voici un petit récapitulatif du mode de fonctionnement :
En résumé, chaque verbe est destiné à une action et la réponse est décrite en plus des données
explicitées par un code de statut.
Pour concevoir une bonne API RESTful, il faut donc toujours se poser ces questions :
— Sur quelle ressource mon opération doit s’effectuer ?
— Quel verbe HTTP décrit le mieux cette opération ?
— Quelle URL permet d’identifier la ressource ?
— Et quel code de statut doit décrire la réponse ?
42
5. FOSRestBundle et Symfony à la
rescousse
Force est de constater que le code dans nos contrôleurs est assez répétitifs. Toutes les réponses
sont en JSON via l’objet JsonResponse, la logique de formatage de celles-ci est dupliqué et
toutes les routes suivent un même modèle.
Nous avons là un schéma classique de code facilement factorisable et justement Symfony nous
propose beaucoup d’outils via les composants et les bundles afin de gérer ce genre de tâches
courantes et/ou répétitifs.
Nous allons donc utiliser les avantages qu’offre le framework Symfony à travers le bundle
FOSRestBundle afin de mieux gérer les problématiques d’implémentation liées au contrainte
REST et gagner ainsi en productivité.
# Réponse
#> ./composer.json has been updated
#> Loading composer repositories with package informatio
#> Updating dependencies (including require-dev)
#> - Installing willdurand/jsonp-callback-validator (v
#> Downloading: 100%
#>
#> - Installing willdurand/negotiation (1.5.0)
#> Downloading: 100%
#>
#> - Installing friendsofsymfony/rest-bundle (2.1.0)
#> Downloading: 100%
43
II. Développement de l’API REST
# app/AppKernel.php
<?php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
// ...
}
À l’état actuel, l’installation n’est pas encore complète. Si nous lançons la commande php
bin/console debug:config fos_rest une belle exception est affichée.
[InvalidArgumentException]
Neither a service called "jms_serializer.serializer" nor
"serializer" is available and no serializer is explicitly
configured. You must either enable the JMSSerializerBundle,
enable the Framework
Bundle serializer or configure a custom serializer.
En effet, pour traiter les réponses, ce bundle a besoin d’un outil de sérialisation.
i
La sérialisation est un processus permettant de convertir des données (une instance d’une
classe, un tableau, etc.) en un format prédéfini. Pour le cas de notre API, la sérialisation
est le mécanisme par lequel nos objets PHP seront transformés en un format textuel
(JSON, XML, etc.).
44
II. Développement de l’API REST
Mais pour nos besoins, le sérialiseur standard suffira largement. Nous allons donc l’activer en
modifiant la configuration de base dans le fichier app/config/config.yml.
# app/config/config.yml
framework:
# ...
serializer:
enabled: true
fos_rest:
disable_csrf_role: null
access_denied_listener:
enabled: false
service: null
formats: { }
unauthorized_challenge: null
param_fetcher_listener:
enabled: false
...
Et voilà !
Le bundle FOSRestBundle fournit un ensemble de fonctionnalités permettant de développer une
API REST. Nous allons en explorer une bonne partie tout au long de ce cours. Mais commençons
d’abord par le système de routage et de gestion des réponses.
Afin de bien voir les effets de nos modifications, nous allons d’abord afficher les routes existantes
avec la commande php bin/console debug:router.
45
II. Développement de l’API REST
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\Place;
46
II. Développement de l’API REST
{
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
/* @var $places Place[] */
$formatted = [];
foreach ($places as $place) {
$formatted[] = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
}
if (empty($place)) {
return new JsonResponse(['message' =>
'Place not found'], Response::HTTP_NOT_FOUND);
}
$formatted = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
47
II. Développement de l’API REST
# app/config/routing.yml
app:
resource: "@AppBundle/Controller/DefaultController.php"
type: annotation
places:
type: rest
resource: AppBundle\Controller\PlaceController
!
Dans la clé app, la déclaration a été changée pour dire à Symfony de ne plus charger
nos contrôleurs REST, la clé app.resource passe ainsi de @AppBundle/Controller à
@AppBundle/Controller/DefaultController.php.
Nous pouvons constater avec la commande php bin/console debug:router que deux routes
ont été générées pour les lieux :
— get_places /places.{_format}
— get_place /place.{_format}
Nous reviendrons plus tard sur la présence de l’attribut _format dans la route.
Il suffit de tester les nouvelles routes générées pour nous rendre compte que le fonctionnement
de l’application reste entièrement le même.
?
Mais comment FOSRestBundle génère-t-il nos routes ?
Tout le secret réside dans des conventions de nommage. Les noms que nous avons utilisé pour le
contrôleur et les actions permettent de générer des routes RESTful sans efforts de notre part.
Ainsi, le nom du contrôleur sans le suffixe Controller permet d’identifier le nom de notre
ressource. PlaceController permet de désigner la ressource places. Il faut noter aussi que si
le contrôleur s’appelait PlacesController (avec un « s »), la ressource serait aussi places. Ce
nom constitue donc le début de notre URL.
Ensuite, pour le reste de l’URL et surtout le verbe HTTP, FOSRestBundle se base sur le nom
de la méthode. La méthode getPlacesAction peut être vu en deux parties : get qui désigne le
verbe HTTP à utiliser GET, et Places au pluriel qui correspond exactement au même nom que
notre ressource.
Cette méthode dit donc à FOSRestBundle que nous voulons récupérer la collection de lieux de
notre application qui le traduit en REST par GET /places.
i
Le paramètre Request $request est propre à Symfony et donc est ignoré par FOSRest-
Bundle.
48
II. Développement de l’API REST
# src/AppBunble/PlaceController.php
<?php
public function getPlaceAction($id, Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($id); // L'identifiant est utilisé
directement
/* @var $place Place */
// ...
Bien que très pratique, le routage automatique peut rapidement montrer ses limites. D’abord, il
nous impose des règles de nommage pour nos méthodes. Si nous voulons nommer autrement nos
actions dans le contrôleur, nous faisons face à une limitation vu que les URL et les verbes HTTP
peuvent être impactés. Ensuite, pour avoir des routes correctes, il faudra connaitre l’ensemble
des règles de nommage qu’utilise FOSTRestBundle, ce qui est loin d’être évident.
Heureusement, nous avons à disposition une méthode manuelle permettant de définir nos routes
facilement.
L’avantage du routage manuel réside dans le fait qu’il se rapproche au plus du système de
routage natif de Symfony avec SensioFrameworkExtraBundle et permet donc de moins se perdre
en tant que débutant. En plus, les annotations permettant de déclarer les routes sont plus
lisibles.
49
II. Développement de l’API REST
# src/AppBunble/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations\Get; // N'oublons pas
d'inclure Get
use AppBundle\Entity\Place;
/**
* @Get("/places")
*/
public function getPlacesAction(Request $request)
{
// ...
}
/**
* @Get("/places/{id}")
*/
public function getPlaceAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id')); // L'identifiant en
tant que paramétre n'est plus nécessaire
// ...
}
}
50
II. Développement de l’API REST
×
Si une de ces annotations est utilisée sur une action du contrôleur, le système de routage
automatique abordé précédemment n’est plus utilisable sur cette même action.
# app/config/config.yml
# ...
fos_rest:
routing_loader:
include_format: false
Si nous relançons php bin/console debug:config fos_rest, le format n’est plus présent
dans les routes :
— get_places GET /places
— get_place GET /places/{id}
Pratiquons en redéfinissant les routes du contrôleur UserController avec les annotations de
FOSRestBundle.
# src/AppBunble/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
51
II. Développement de l’API REST
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations\Get;
use AppBundle\Entity\User;
/**
* @Get("/users/{user_id}")
*/
public function getUserAction(Request $request)
{
// ...
}
}
# app/config/routing.yml
app:
resource: "@AppBundle/Controller/DefaultController.php"
type: annotation
places:
type: rest
resource: AppBundle\Controller\PlaceController
users:
type: rest
resource: AppBundle\Controller\UserController
Voyons maintenant les outils que ce bundle nous propose pour la gestion des vues.
52
II. Développement de l’API REST
Avec FOSRestBundle, nous disposons d’un service appelé fos_rest.view_handler qui nous
permet de gérer nos réponses. Pour l’utiliser, il suffit d’instancier une vue FOSRestBundle,
la configurer et laisser le gestionnaire de vue (le view handler) s’occuper du reste. Voyez par
vous-même :
# src/AppBunble/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\View\ViewHandler;
use FOS\RestBundle\View\View; // Utilisation de la vue de
FOSRestBundle
use AppBundle\Entity\Place;
/**
* @Get("/places")
*/
public function getPlacesAction(Request $request)
{
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
/* @var $places Place[] */
$formatted = [];
foreach ($places as $place) {
$formatted[] = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
}
53
II. Développement de l’API REST
// Gestion de la réponse
return $viewHandler->handle($view);
}
}
L’intérêt d’utiliser un bundle réside aussi dans le fait de réduire les lignes de codes que nous
avons à écrire (et par la même occasion, les sources de bogues). N’hésitez pas à retester notre
appel afin de vérifier que la réponse est toujours la même.
FOSRestBundle introduit aussi un listener (ViewResponseListener) qui nous permet, à l’instar
de Symfony via l’annotation Template du SensioFrameworkExtraBundle , de renvoyer juste
une instance de View et laisser le bundle appelait le gestionnaire de vue lui-même.
×
Pour utiliser l’annotation View, il faut que le SensioFrameworkExtraBundle soit activé.
Mais si vous avez utilisé l’installateur de Symfony pour créer ce projet, c’est déjà le cas.
# app/config/config.yml
fos_rest:
routing_loader:
include_format: false
view:
view_response_listener: true
Ensuite, il ne reste plus qu’à adapter le code (toutes les annotations de FOSRestBundle seront
aliasées par Rest) :
# src/AppBunble/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use FOS\RestBundle\View\ViewHandler;
54
II. Développement de l’API REST
/**
* @Rest\View()
* @Rest\Get("/places")
*/
public function getPlacesAction(Request $request)
{
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
/* @var $places Place[] */
$formatted = [];
foreach ($places as $place) {
$formatted[] = [
'id' => $place->getId(),
'name' => $place->getName(),
'address' => $place->getAddress(),
];
}
return $view;
}
}
La simplicité qu’apporte ce bundle ne s’arrête pas là. Les données assignées à la vue sont
sérialisées au bon format en utilisant le sérialiseur que nous avions configuré au début. Ce
sérialiseur supporte aussi bien les tableaux que les objets. Si vous voulez approfondir le sujet, il
est préférable de consulter la documentation complète .
Ce qu’il faut retenir dans notre cas, c’est qu’avec nos objets actuels (accesseurs en visibilité
public), le sérialiseur de Symfony peut les transformer pour nous. Au lieu de passer un tableau
formaté par nos soins, nous allons passer directement une liste d’objets au view handler. Notre
code peut être réduit à :
# src/AppBunble/PlaceController.php
<?php
namespace AppBundle\Controller;
55
II. Développement de l’API REST
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use FOS\RestBundle\View\ViewHandler;
use FOS\RestBundle\View\View; // Utilisation de la vue de
FOSRestBundle
use AppBundle\Entity\Place;
/**
* @Rest\View()
* @Rest\Get("/places")
*/
public function getPlacesAction(Request $request)
{
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
/* @var $places Place[] */
return $view;
}
}
Et là, nous voyons vraiment l’intérêt d’utiliser les composants que nous propose le framework.
L’objectif est d’être le plus concis et productif possible.
Pour l’instant, notre API ne supporte qu’un seul format : le JSON. Donc au lieu de le mettre
dans tous les contrôleurs, FOSRestBundle propose un mécanisme permettant de gérer les formats
et la négociation de contenu : le format listener .
Il y aura un chapitre dédié à la gestion de plusieurs formats et la négociation de contenu.
Pour l’instant, nous allons juste configurer le format listener de FOSRestBundle pour que toutes
les URL renvoient du JSON.
56
II. Développement de l’API REST
# src/app/config/config.yml
fos_rest:
routing_loader:
include_format: false
view:
view_response_listener: true
format_listener:
rules:
- { path: '^/', priorities: ['json'], fallback_format:
'json' }
La seule règle déclarée dit que pour toutes les URL (path: ^/), le format prioritaire est le JSON
(priorities: ['json']) et si aucun format n’est demandé par le client, il faudra utiliser le
JSON quand même (fallback_format: 'json').
Vu que maintenant nous n’avons plus à définir le format dans les actions de nos contrôleurs,
nous avons même la possibilité de renvoyer directement nos objets sans utiliser l’objet View de
FOSRestBundle.
# src/AppBunble/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\Place;
/**
* @Rest\View()
* @Rest\Get("/places")
*/
public function getPlacesAction(Request $request)
{
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
/* @var $places Place[] */
return $places;
}
57
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
# src/AppBunble/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\Place;
/**
* @Rest\View()
* @Rest\Get("/places")
*/
public function getPlacesAction(Request $request)
{
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
/* @var $places Place[] */
return $places;
}
58
II. Développement de l’API REST
/**
* @Rest\View()
* @Rest\Get("/places/{id}")
*/
public function getPlaceAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id')); // L'identifiant en
tant que paramétre n'est plus nécessaire
/* @var $place Place */
if (empty($place)) {
return new JsonResponse(['message' =>
'Place not found'], Response::HTTP_NOT_FOUND);
}
return $place;
}
}
# src/AppBunble/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\User;
59
II. Développement de l’API REST
return $users;
}
/**
* @Rest\View()
* @Rest\Get("/users/{user_id}")
*/
public function getUserAction(Request $request)
{
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->find($request->get('user_id'));
/* @var $user User */
if (empty($user)) {
return new JsonResponse(['message' =>
'User not found'], Response::HTTP_NOT_FOUND);
}
return $user;
}
}
FOSRestBundle est l’un des bundles les plus connus pour faire une API REST avec Symfony.
Bien qu’ayant abordé pas mal de points dans cette partie du cours, il reste encore beaucoup
de fonctionnalités à découvrir et durant ce cours une bonne partie sera présentée. Mais la
référence reste la documentation officielle qui vous sera d’une grande aide dans vos futurs
développements.
Pour le reste du cours, nous utiliserons ce bundle pour faciliter le travail et ne pas réinventer la
roue. Le routage et la gestion des réponses seront calqués sur les cas que nous venons de voir.
60
6. Créer et supprimer des ressources
Notre API ne permet pour l’instant que la lecture de données. Une API en lecture seule étant
loin d’être courante (ni amusante à développer), nous allons voir comment créer et supprimer
une ressource en suivant les principes REST.
Pour concevoir une bonne API RESTful, il faut donc toujours se poser ces questions :
— Sur quelle ressource mon opération doit s’effectuer ?
— Quel verbe HTTP décrit le mieux cette opération ?
— Quelle URL permet d’identifier la ressource ?
— et quel code de statut doit décrire la réponse ?
Nous allons donc suivre ce conseil, et rajouter une action permettant de créer un lieu dans notre
application.
?
La première question que nous devons nous poser est sur quelle ressource pouvons-nous
faire un appel de création ?
De point de vue sémantique, nous pouvons considérer qu’une entité dans une application est
accessible en utilisant la collection (places) ou en utilisant directement la ressource à travers
son identifiant (places/1). Mais comme vous vous en doutez, une ressource que nous n’avons
pas encore créé ne peut pas avoir d’identifiant.
Il faut donc voire la création d’une ressource comme étant l’ajout de celle-ci dans une collection.
Créer un lieu revient donc à rajouter un lieu à notre liste déjà existante. Pour créer une ressource,
il faudra donc utiliser la collection associée.
61
II. Développement de l’API REST
Pour identifier notre collection, nous utiliserons l’URL rest-api.local/places. Mais quel
appel doit-on faire ? Les verbes HTTP ont chacun une signification et une utilisation bien définie.
Pour la création, la méthode POST est bien appropriée. Pour s’en convaincre, il suffit de consulter
la RFC 7231 qui dit :
For example, POST is used for the following functions (among others
— Providing a block of data, such as the fields entered into an HTML form, to a
data-handling process ;
— Posting a message to a bulletin board, newsgroup, mailing list, blog, or similar group
of articles ;
— Creating a new resource that has yet to be identified by the origin server ;
-
Maintenant que nous savons qu’il faudra une requête du type POST rest-api.local/places,
nous allons nous intéresser au corps de notre requête : le payload (dans le jargon API).
Lorsque nous soumettons un formulaire sur une page web avec la méthode POST, le contenu est
encodé en utilisant les encodages application/x-www-form-urlencoded ou encore multipart/form-
data que vous avez sûrement déjà rencontrés.
Pour le cas d’une API, nous pouvons utiliser le format que nous voulons dans le corps de nos
requêtes tant que le serveur supporte ce format. Nous allons donc choisir le JSON comme
format.
i
Ce choix n’est en aucun cas lié au format de sortie de nos réponses. Le JSON reste un
format textuel largement utilisé et supporté et représente souvent le minimum à supporter
par une API REST. Ceci étant dit, supporter le format JSON n’est pas une contrainte
REST.
Pour rappels, les codes de statut HTTP peuvent être regroupés par famille. Le premier chiffre
permet d’identifier la famille de chaque code. Ainsi les codes de la famille 2XX (200, 201, 204,
etc.) décrivent une requête qui s’est effectué avec succès, la famille 4XX (400, 404, etc.) pour une
erreur côté client et enfin la famille 5XX (500, etc.) pour une erreur serveur. La liste complète
des codes de statut et leur signification est disponible dans la section 6 de la RFC 7231 . Mais
62
II. Développement de l’API REST
pour notre cas, une seule nous intéresse : 201 Created. Le message associé à ce code parle de
lui-même, si une ressource a été créée avec succès, nous utiliserons donc le code 201.
http://zestedesavoir.com/media/galleries/3183/
Mettons en pratique tout cela en donnant la possibilité aux utilisateurs de notre API de créer
un lieu. Un utilisateur devra faire une requête POST sur l’URL rest-api.local/places avec
comme payload :
{
"name": "ici un nom",
"address": "ici une adresse"
}
!
Le corps de la requête ne contient pas l’identifiant vu que nous allons le créer côté serveur.
Pour des soucis de clarté, les méthodes déjà existantes dans le contrôleur PlaceController
ne seront pas visibles dans les extraits de code. Commençons donc par créer une route et en
configurant le routage comme il faut :
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\Place;
63
II. Développement de l’API REST
* @Rest\Post("/places")
*/
public function postPlacesAction(Request $request)
{
}
}
Pour tester la méthode, nous allons tout d’abord simplement renvoyer les informations qui
seront dans le payload.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\Place;
http://zestedesavoir.com/media/galleries/3183/
64
II. Développement de l’API REST
Il faut choisir comme contenu JSON, Postman rajoutera automatiquement l’entête Content-
Type qu’il faut à la requête. Nous explorerons plus en détails ces entêtes plus tard dans ce
cours.
http://zestedesavoir.com/media/galleries/3183/
http://zestedesavoir.com/media/galleries/3183/
Nous avons maintenant un système opérationnel pour récupérer les informations pour créer
notre lieu. Mais avant de continuer, un petit aparté sur FOSRestBundle s’impose.
Il faut savoir que de base, Symfony ne peut pas peupler les paramétres de l’objet Request
avec le payload JSON. Dans une application n’utilisant pas FOSRestBundle, il faudrait parser
manuellement le contenu en faisant json_decode($request->getContent(), true) pour
accéder au nom et à l’adresse du lieu.
Pour s’en convaincre, nous allons désactiver le body listener qui est activé par défaut.
# app/config/config.yml
fos_rest:
routing_loader:
include_format: false
view:
view_response_listener: true
format_listener:
rules:
- { path: '^/', priorities: ['json'], fallback_format:
'json', prefer_extension: false }
# configuration à rajouter pour désactiver le body listener
65
II. Développement de l’API REST
body_listener:
enabled: false
http://zestedesavoir.com/media/galleries/3183/
<?php
return [
'payload' => json_decode($request->getContent(), true)
];
body_listener:
enabled: true
Maintenant que nous avons les informations nécessaires pour créer un lieu, nous allons juste
l’insérer en base avec Doctrine. Pour définir le bon code de statut, il suffit de mettre un paramètre
statusCode=Response::HTTP_CREATED dans l’annotation View.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
66
II. Développement de l’API REST
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\Place;
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($place);
$em->flush();
return $place;
}
}
Ici, en renvoyant la ressource qui vient d’être créée, nous suivons la RFC 7231 .
The 201 response payload typically describes and links to the resource(s) created.
Pour les tester notre implémentation, nous allons utiliser :
{
"name": "Disneyland Paris",
"address": "77777 Marne-la-Vallée"
}
http://zestedesavoir.com/media/galleries/3183/
67
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Bien que nous puissions créer avec succès un lieu, nous n’effectuons aucune validation. Dans
cette partie, nous allons voir comment valider les informations en utilisant les formulaires de
Symfony.
Nous allons commencer par créer un formulaire pour les lieux :
# src/AppBundle/Form/Type/PlaceType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
Dans une API, il faut obligatoirement désactiver la protection CSRF (Cross-Site Request
Forgery). Nous n’utilisons pas de session et l’utilisateur de l’API peut appeler cette méthode
sans se soucier de l’état de l’application : l’API doit rester sans état : stateless.
Nous allons maintenant rajouter des contraintes simples pour notre lieu. Le nom et l’adresse ne
doivent pas être nulles et en plus, le nom doit être unique. Nous utiliserons le format YAML
pour les règles de validations.
68
II. Développement de l’API REST
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Place:
constraints:
-
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
name
properties:
name:
- NotBlank: ~
- Type: string
address:
- NotBlank: ~
- Type: string
Jusque-là rien de nouveau avec les formulaires Symfony. Si ce code ne vous parait pas assez
claire. Il est préférable de consulter la documentation officielle avant de continuer ce cours.
i
Vu que nous avons une contrainte d’unicité sur le champ name. Il est plus logique de
rajouter cela dans les annotations Doctrine.
# src/AppBundle/Entity/Place.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="places",
* uniqueConstraints={@ORM\UniqueConstraint(name="places_name_unique",colu
* )
*/
class Place
{
// ...
}
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
69
II. Développement de l’API REST
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($place);
$em->flush();
return $place;
} else {
return $form;
}
}
}
Le format des données attendu lorsqu’on utilise la méthode handleRequest des formulaires
Symfony est un peu différent de celui que nous utilisons pour créer un lieu. Avec handleRequest,
nous aurions dû utiliser :
{
"place":
{
"name": "ici un nom",
"address": "ici une adresse"
}
}
70
II. Développement de l’API REST
Donc pour mieux répondre aux contraintes REST, au lieu d’utiliser la méthode handleRequest
pour soumettre le formulaire, nous avons opté pour la soumission manuelle avec submit.
Nous adaptons Symfony à REST et pas l’inverse.
Lorsque le formulaire n’est pas valide, nous nous contentons juste de renvoyer le formulaire. Le
ViewHandler de FOSRestBundle est conçu pour gérer nativement les formulaires invalides.
Non seulement, il est en mesure de formater les erreurs dans le formulaire mais en plus, il renvoie
le bon code de statut lorsque les données soumises sont invalide : 400. Le code de statut 400
permet de signaler au client de l’API que sa requête est invalide.
Pour s’en assurer, essayons de recréer un autre lieu avec les mêmes informations que le précé-
dent.
http://zestedesavoir.com/media/galleries/3183/
{
"code": 400,
"message": "Validation Failed",
"errors": {
"children": {
"name": {
"errors": [
"This value is already used."
]
},
"address": []
}
}
}
Si la clé errors d’un attribut existe, alors il y a des erreurs de validation sur cet attribut.
Comme pour les lieux, nous allons créer une action permettant de rajouter un utilisateur à
notre application. Nous aurons comme contraintes :
— le prénom, le nom et l’adresse mail de l’utilisateur ne doivent pas être nuls ;
71
II. Développement de l’API REST
{
"firstname": "",
"lastname": "",
"email": ""
}
Comme pour les lieux, pour créer un utilisateur il faudra une requête POST sur l’URL rest-
api.local/users qui désigne notre collection d’utilisateurs.
Allons-y !
Configuration du formulaire et des contraintes de validation :
# src/AppBundle/Form/Type/UserType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
72
II. Développement de l’API REST
# src/AppBundle/Resources/config/validation.yml
# ...
AppBundle\Entity\User:
constraints:
-
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
email
properties:
firstname:
- NotBlank: ~
- Type: string
lastname:
- NotBlank: ~
- Type: string
email:
- NotBlank: ~
- Email: ~
# src/AppBundle/Entity/User.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="users",
* uniqueConstraints={@ORM\UniqueConstraint(name="users_email_unique",colum
* )
*/
class User
{
// ...
}
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
73
II. Développement de l’API REST
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\UserType;
use AppBundle\Entity\User;
/**
* @Rest\View(statusCode=Response::HTTP_CREATED)
* @Rest\Post("/users")
*/
public function postUsersAction(Request $request)
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($user);
$em->flush();
return $user;
} else {
return $form;
}
}
}
?
Que voulons-nous supprimer ?
74
II. Développement de l’API REST
La méthode DELETE s’appliquera sur la ressource à supprimer. Si par exemple nous voulons
supprimer le lieu avec comme identifiant 3, il suffira de faire une requête sur l’URL rest-
api.local/places/3.
Une fois n’est pas de coutume, nous allons consulter la RFC 7312
If a DELETE method is successfully applied, the origin server SHOULD send a 202 (Accepted)
status code if the action will likely succeed but has not yet been enacted, a 204 (No Content)
status code if the action has been enacted and no further information is to be supplied, or a
200 (OK) status code if the action has been enacted and the response message includes a
representation describing the status.
Cette citation est bien longue mais ce qui nous intéresse ici se limite à a 204 (No Content)
status code if the action has been enacted and no further information is to be
supplied. Pour notre cas, lorsque la ressource sera supprimée, nous allons renvoyer aucune
information. Le code de statut à utiliser est donc : 204.
http://zestedesavoir.com/media/galleries/3183/
Nous allons, sans plus attendre, créer une méthode pour supprimer un lieu de notre application.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
/**
* @Rest\View(statusCode=Response::HTTP_NO_CONTENT)
75
II. Développement de l’API REST
* @Rest\Delete("/places/{id}")
*/
public function removePlaceAction(Request $request)
{
$em = $this->get('doctrine.orm.entity_manager');
$place = $em->getRepository('AppBundle:Place')
->find($request->get('id'));
/* @var $place Place */
$em->remove($place);
$em->flush();
}
}
http://zestedesavoir.com/media/galleries/3183/
http://zestedesavoir.com/media/galleries/3183/
?
Que faire si nous essayons de supprimer une ressource qui n’existe pas ou plus ?
Si nous essayons de supprimer à nouveau la même ressource, nous obtenons une erreur interne.
Mais, il se trouve que dans les spécifications de la méthode DELETE, il est dit que cette
méthode doit être idempotente.
i
Une action idempotente est une action qui produit le même résultat et ce, peu importe le
nombre de fois qu’elle est exécutée.
Pour suivre ces spécifications HTTP, nous allons modifier notre code pour gérer le cas où le lieu
à supprimer n’existe pas ou plus. En plus, l’objectif d’un client qui fait un appel de suppression
76
II. Développement de l’API REST
est de supprimer une ressource, donc si elle l’est déjà, nous pouvons considérer que tout c’est
bien passé.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
if ($place) {
$em->remove($place);
$em->flush();
}
}
}
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
77
II. Développement de l’API REST
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\UserType;
use AppBundle\Entity\User;
/**
* @Rest\View(statusCode=Response::HTTP_NO_CONTENT)
* @Rest\Delete("/users/{id}")
*/
public function removeUserAction(Request $request)
{
$em = $this->get('doctrine.orm.entity_manager');
$user = $em->getRepository('AppBundle:User')
->find($request->get('id'));
/* @var $user User */
if ($user) {
$em->remove($user);
$em->flush();
}
}
}
Pour revenir sur nos tableaux récapitulatifs, voici le mode de fonctionnement simplifié d’une
API REST :
78
II. Développement de l’API REST
79
7. Mettre à jour des ressources
Maintenant que nous pouvons lire, écrire et supprimer des ressources, il ne reste plus qu’à
apprendre à les modifier et le CRUD1 (Créer, Lire, Mettre à jour et Supprimer ) en REST
n’aura plus de secret pour nous.
Dans cette partie, nous aborderons les concepts liés à la mise à jour de ressources REST et nous
ferons un petit détour sur la gestion des erreurs avec FOSRestBundle.
Lorsque nous voulons modifier une ressource, la question ne se pose pas. La cible de notre
requête est la ressource à mettre à jour. Donc pour mettre à jour un lieu, nous devrons faire
une requête sur l’URL de celle-ci (par exemple rest-api.local/places/1).
La différenciation entre la mise à jour complète ou partielle d’une ressource se fait avec le choix
du verbe HTTP utilisé. Donc le verbe est ici d’une importance capitale.
Notre fameuse RFC 7231 décrit la méthode PUT comme :
The PUT method requests that the state of the target resource be created or replaced with
the state defined by the representation enclosed in the request message payload.
La méthode PUT permet de créer ou de remplacer une ressource.
Le cas d’utilisation de PUT pour créer une ressource est très rare et nous ne l’aborderons pas. Il
faut juste retenir que pour que cet verbe soit utilisé pour créer une ressource, il faudrait laisser
au client de l’API le choix des URL de nos ressources. Nous l’utiliserons donc juste afin de
80
II. Développement de l’API REST
remplacer le contenu d’une ressource par le payload de la requête, bref pour la mettre à jour en
entier.
Le corps de la requête sera donc la nouvelle valeur que nous voulons affecter à notre ressource
(toujours au format JSON comme pour la création).
Dans la description même de la requête PUT, le code de statut à utiliser est explicité : 200.
A successful PUT of a given representation would suggest that a subsequent GET on that
same target resource will result in an equivalent representation being sent in a 200 (OK)
response.
http://zestedesavoir.com/media/galleries/3183/
i
Juste pour rappel, comme pour la récupération d’une ressource, si le client essaye de
mettre à jour une ressource inexistante, nous aurons un 404.
Pour une mise à jour complète, un utilisateur devra faire une requête PUT sur l’URL rest-
api.local/places/{id} où {id} représente l’identifiant du lieu avec comme payload, le
même qu’à la création :
{
"name": "ici un nom",
"address": "ici une adresse"
}
×
Le corps de la requête ne contient pas l’identifiant de la ressource vu qu’elle sera disponible
dans l’URL.
81
II. Développement de l’API REST
Le routage dans notre contrôleur se rapproche beaucoup de celle pour récupérer un lieu :
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Entity\Place;
}
}
Les règles de validation des informations sont exactement les mêmes qu’à la création d’un lieu.
Nous allons donc exploiter le même formulaire Symfony. La seule différence ici réside dans le
fait que nous devons d’abord récupérer une instance du lieu dans la base de données avant
d’appliquer les mises à jour.
# src/AppController/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
82
II. Développement de l’API REST
/**
* @Rest\View()
* @Rest\Put("/places/{id}")
*/
public function updatePlaceAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id')); // L'identifiant en
tant que paramètre n'est plus nécessaire
/* @var $place Place */
if (empty($place)) {
return new JsonResponse(['message' =>
'Place not found'], Response::HTTP_NOT_FOUND);
}
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
// l'entité vient de la base, donc le merge n'est pas
nécessaire.
// il est utilisé juste par soucis de clarté
$em->merge($place);
$em->flush();
return $place;
} else {
return $form;
}
}
}
http://zestedesavoir.com/media/galleries/3183/
La réponse est :
83
II. Développement de l’API REST
{
"id": 2,
"name": "Mont-Saint-Michel",
"address": "Autre adresse Le Mont-Saint-Michel"
}
http://zestedesavoir.com/media/galleries/3183/
La mise à jour complète d’un utilisateur suit exactement le même modèle que celle d’un lieu.
Les contraintes de validation sont identiques à celles de la création d’un utilisateur.
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\UserType;
use AppBundle\Entity\User;
/**
* @Rest\View()
* @Rest\Put("/users/{id}")
*/
public function updateUserAction(Request $request)
{
$user = $this->get('doctrine.orm.entity_manager')
84
II. Développement de l’API REST
->getRepository('AppBundle:User')
->find($request->get('id')); // L'identifiant en
tant que paramètre n'est plus nécessaire
/* @var $user User */
if (empty($user)) {
return new JsonResponse(['message' =>
'User not found'], Response::HTTP_NOT_FOUND);
}
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
// l'entité vient de la base, donc le merge n'est pas
nécessaire.
// il est utilisé juste par soucis de clarté
$em->merge($user);
$em->flush();
return $user;
} else {
return $form;
}
}
}
?
Que se passe-t-il si nous faisons une requête en omettant le champ address ?
{
"name": "Autre-Mont-Saint-Michel"
}
{
"code": 400,
"message": "Validation Failed",
"errors": {
"children": {
"name": [],
85
II. Développement de l’API REST
"address": {
"errors": [
"This value should not be blank."
]
}
}
}
}
Cela nous permet de bien valider les données envoyées par le client mais avec cette méthode, il
est dans l’obligation de connaitre tous les champs afin d’effectuer sa mise à jour.
?
Que faire alors si nous voulons modifier par exemple que le nom d’un lieu ?
Jusqu’à présent, nos appels API pour modifier une ressource se contente de la remplacer par une
nouvelle (en gardant l’identifiant). Mais dans une API plus complète avec des ressources avec
beaucoup d’attributs, nous pouvons rapidement sentir le besoin de modifier juste quelques-uns
de ces attributs.
Pour cela, la seule chose que nous devons changer dans notre API c’est le verbe HTTP utilisé.
Parmi toutes les méthodes que nous avons déjà pu utiliser, PATCH est la seule qui n’est pas
spécifiée dans la RFC 7231 mais plutôt dans la RFC 5789 .
Ce standard n’est pas encore validé - PROPOSED STANDARD (au moment où ces lignes sont
écrites) - mais est déjà largement utilisé.
Cette méthode doit être utilisée pour décrire un ensemble de changements à appliquer à la
ressource identifiée par son URI.
?
Comment décrire les changements à appliquer ?
Vu que nous utilisons du JSON dans nos payload. Il existe une RFC 6902 , elle aussi pas
encore adoptée, qui essaye de formaliser le payload d’une requête PATCH utilisant du JSON.
Par exemple, dans la section 4.3 , nous pouvons lire la description d’une opération consistant
à remplacer un champ d’une ressource :
86
II. Développement de l’API REST
Pour le cas de notre lieu, si nous voulions un correctif (patch) pour ne changer que l’adresse, il
faudrait :
Outre le fait que cette méthode n’est pas beaucoup utilisée, sa mise en œuvre par un client est
complexe et son traitement coté serveur l’est autant.
Donc par pragmatisme, nous n’allons pas utiliser PATCH de cette façon.
Dans notre API, une requête PATCH aura comme payload le même que celui d’une requête POST
à une grande différence près : Si un attribut n’existe pas dans le corps de la requête, nous devons
conserver son ancienne valeur.
Notre requête avec comme payload :
{
"name": "Autre-Mont-Saint-Michel"
}
... ne devra pas renvoyer une erreur mais juste modifier le nom de notre lieu.
http://zestedesavoir.com/media/galleries/3183/
L’implémentation de la mise à jour partielle avec Symfony est très proche de la mise à jour
complète. Il suffit de rajouter un paramètre dans la méthode submit (clearMissing = false)
et le tour est joué. Comme son nom l’indique, avec clearMissing à false, Symfony conservera
tous les attributs de l’entité Place qui ne sont pas présents dans le payload de la requête.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
87
II. Développement de l’API REST
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
/**
* @Rest\View()
* @Rest\Patch("/places/{id}")
*/
public function patchPlaceAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id')); // L'identifiant en
tant que paramètre n'est plus nécessaire
/* @var $place Place */
if (empty($place)) {
return new JsonResponse(['message' =>
'Place not found'], Response::HTTP_NOT_FOUND);
}
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
// l'entité vient de la base, donc le merge n'est pas
nécessaire.
// il est utilisé juste par soucis de clarté
$em->merge($place);
$em->flush();
return $place;
} else {
return $form;
}
88
II. Développement de l’API REST
}
}
×
Nous avons ici un gros copier-coller de la méthode updatePlace, un peu de refactoring
ne sera pas de mal.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
/**
* @Rest\View()
* @Rest\Put("/places/{id}")
*/
public function updatePlaceAction(Request $request)
{
return $this->updatePlace($request, true);
}
/**
* @Rest\View()
* @Rest\Patch("/places/{id}")
*/
public function patchPlaceAction(Request $request)
{
return $this->updatePlace($request, false);
}
89
II. Développement de l’API REST
->find($request->get('id')); // L'identifiant en
tant que paramètre n'est plus nécessaire
/* @var $place Place */
if (empty($place)) {
return new JsonResponse(['message' =>
'Place not found'], Response::HTTP_NOT_FOUND);
}
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($place);
$em->flush();
return $place;
} else {
return $form;
}
}
}
http://zestedesavoir.com/media/galleries/3183/
http://zestedesavoir.com/media/galleries/3183/
90
II. Développement de l’API REST
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\UserType;
use AppBundle\Entity\User;
/**
* @Rest\View()
* @Rest\Patch("/users/{id}")
*/
public function patchUserAction(Request $request)
{
return $this->updateUser($request, false);
}
if (empty($user)) {
return new JsonResponse(['message' =>
'User not found'], Response::HTTP_NOT_FOUND);
}
$form->submit($request->request->all(), $clearMissing);
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
91
II. Développement de l’API REST
$em->persist($user);
$em->flush();
return $user;
} else {
return $form;
}
}
}
Au fur et à mesure de nos développements, nous nous rendons compte que notre API est très
uniforme, donc facile à utiliser pour un client. Mais aussi l’implémentation serveur l’est autant.
Cette uniformité facilite grandement le développement d’une API RESTful et notre productivité
est décuplée !
Jusque-là, nous utilisons un objet JsonResponse lorsque la ressource recherchée n’existe pas.
Cela fonctionne bien mais nous n’utilisons pas FOSRestBundle de manière appropriée. Au lieu
de renvoyer une réponse JSON, nous allons juste renvoyer une vue FOSRestBundle et laisser
le view handler le formater en JSON. En procédant ainsi, nous pourrons plus tard exploiter
toutes les fonctionnalités de ce bundle comme par exemple changer le format des réponses (par
exemple renvoyer du XML) sans modifier notre code. Pour ce faire, il suffit de remplacer toutes
les lignes :
Par
92
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Nous pouvons facilement affirmer que notre application est au niveau 2 vu que nous exploitons
les différents verbes que propose le protocole HTTP pour interagir avec des ressources identifiées
par des URIs. Nous verrons plus tard comment s’approcher du niveau 3 en exploitant d’autres
bundles de Symfony à notre disposition.
Nos tableaux récapitulatifs s’étoffent encore plus et nous pouvons rajouter les opérations de
mise à jour.
93
8. Relations entre ressources
Maintenant que nous pouvons effectuer toutes les opérations CRUD (Créer, Lire, Mettre à jour
et Supprimer) sur nos ressources, qu’est ce qui pourrait rester pour avoir une API pleinement
fonctionnelle ?
Actuellement, nous avons une liste d’utilisateurs d’un côté et une liste de lieux d’un autre. Mais
l’objectif de notre application est de pouvoir proposer à chaque utilisateur, selon ses centres
d’intérêts, une idée de sortie en utilisant les différents lieux référencés.
Nous pouvons imaginer qu’un utilisateur passionné d’histoire ou d’architecture irait plutôt
visiter un musée, un château, etc. Ou encore, selon le budget dont nous disposons, le tarif pour
accéder à ces différents lieux pourrait être un élément important.
En résumé, nos ressources doivent avoir des relations entre elles et c’est ce que nous allons
aborder dans cette partie du cours.
Supposons qu’un lieu ait un ou plusieurs tarifs par exemple moins de 12 ans et tout public. En
termes de conception de la base de données, une relation oneToMany permet de gérer facilement
cette situation et donc d’interagir avec les tarifs d’un lieu donné.
?
Comment matérialiser une telle relation avec une API qui suit les contraintes REST ?
Si nous créons une URI nommée rest-api.local/prices, nous pouvons effectivement accéder
à nos prix comme pour les lieux ou les utilisateurs. Mais nous aurons accès à l’ensemble des
tarifs appliqués pour tous les lieux de notre application.
Pour accéder aux prix d’un lieu 1, il serait tentant de rajouter un paramètre du style rest-
api.local/prices?place_id=1 mais, la répétition étant pédagogique, nous allons regarder
à nouveau le deuxième chapitre ”Premières interactions avec les ressources” :
la RFC 3986 spécifie clairement les query strings comme étant des composants qui
contiennent des données non-hiérarchiques.
Nous avons une notion d’hièrarchie entre un lieu et ses tarifs et donc cette relation doit apparaitre
dans notre URI.
94
II. Développement de l’API REST
rest-api.local/prices/1 ferait-il l’affaire ? Sûrement pas, cette URL désigne le tarif ayant
comme identifiant 1.
Pour trouver la bonne URL, nous devons commencer par le possesseur dans la relation ici c’est
un lieu qui a des tarifs, donc rest-api.local/places/{id} doit être le début de notre URL.
Ensuite, il suffit de rajouter l’identifiant de la collection de prix que nous appelerons prices.
En définitif, rest-api.local/places/{id}/prices permet de désigner clairement les tarifs
pour le lieu ayant comme identifiant {id}.
http://zestedesavoir.com/media/galleries/3183/
Une fois que nous avons identifié notre ressource, tous les principes déjà abordés pour interagir
avec une ressource s’appliquent.
Pour mettre en pratique toutes ces informations, nous allons ajouter deux nouveaux appels à
notre API :
— un pour créer un nouveau prix ;
— un pour lister tous les prix d’un lieu.
Nous considérons qu’un prix a deux caractéristiques :
— un type (tout public, moins de 12 ans, etc.) ;
— une valeur (10, 15.5, 22.75, 29.99, etc.) qui désigne le tarif en euros.
Pour l’instant, seul deux types de prix sont supportés :
95
II. Développement de l’API REST
# src/AppBundle/Entity/Price.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="prices",
* uniqueConstraints={@ORM\UniqueConstraint(name="prices_type_place_unique
* )
*/
class Price
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $type;
/**
* @ORM\Column(type="float")
*/
protected $value;
/**
* @ORM\ManyToOne(targetEntity="Place", inversedBy="prices")
* @var Place
*/
protected $place;
Nous utilisons une relation bidirectionnelle car nous voulons afficher les prix d’un lieu en plus
des informations de base lorsqu’un client de l’API consulte les informations de ce lieu.
96
II. Développement de l’API REST
# src/AppBundle/Entity/Place.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="places",
* uniqueConstraints={@ORM\UniqueConstraint(name="places_name_unique",colu
* )
*/
class Place
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="string")
*/
protected $address;
/**
* @ORM\OneToMany(targetEntity="Price", mappedBy="place")
* @var Price[]
*/
protected $prices;
97
II. Développement de l’API REST
La création d’un prix nécessite quelques règles de validation que nous devons implémenter.
# src/AppBundle/Resources/config/validation.yml
# ...
AppBundle\Entity\Price:
properties:
type:
- NotNull: ~
- Choice:
choices: [less_than_12, for_all]
value:
- NotNull: ~
- Type: numeric
- GreaterThanOrEqual:
value: 0
# src/AppBundle/Form/Type/PriceType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
98
II. Développement de l’API REST
Les deux appels seront mis en place dans un nouveau contrôleur pour des raisons de clarté.
Mais il est parfaitement possible de le mettre dans le contrôleur déjà existant. Nous aurons
un nouveau dossier nommé src/AppBundle/Controller/Place qui contiendra un contrôleur
PriceController.
Avec ce découpage des fichiers, nous mettons en évidence la relation hiérarchique entre Place et
Price.
# src/AppBundle/Controller/Place/PriceController.php
<?php
namespace AppBundle\Controller\Place;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
/**
* @Rest\View()
* @Rest\Get("/places/{id}/prices")
*/
public function getPricesAction(Request $request)
{
/**
* @Rest\View(statusCode=Response::HTTP_CREATED)
* @Rest\Post("/places/{id}/prices")
*/
public function postPricesAction(Request $request)
{
}
}
99
II. Développement de l’API REST
# app/config/routing.yml
app:
resource: "@AppBundle/Controller/DefaultController.php"
type: annotation
places:
type: rest
resource: AppBundle\Controller\PlaceController
prices:
type: rest
resource: AppBundle\Controller\Place\PriceController
users:
type: rest
resource: AppBundle\Controller\UserController
Au niveau des URL utilisées dans le routage, il suffit de se référer au tableau plus haut. Finissons
notre implémentation en ajoutant de la logique aux actions du contrôleur :
# src/AppBundle/Controller/Place/PriceController.php
<?php
namespace AppBundle\Controller\Place;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PriceType;
use AppBundle\Entity\Price;
/**
* @Rest\View()
* @Rest\Get("/places/{id}/prices")
*/
public function getPricesAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id')); // L'identifiant en
tant que paramétre n'est plus nécessaire
/* @var $place Place */
100
II. Développement de l’API REST
if (empty($place)) {
return $this->placeNotFound();
}
return $place->getPrices();
}
/**
* @Rest\View(statusCode=Response::HTTP_CREATED)
* @Rest\Post("/places/{id}/prices")
*/
public function postPricesAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id'));
/* @var $place Place */
if (empty($place)) {
return $this->placeNotFound();
}
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($price);
$em->flush();
return $price;
} else {
return $form;
}
}
101
II. Développement de l’API REST
Le principe reste le même qu’avec les différentes actions que nous avons déjà implémentées. Il
faut juste noter que lorsque nous créons un prix, nous pouvons lui associer un lieu en récupérant
l’identifiant du lieu qui est dans l’URL de la requête.
Pour tester nos nouveaux appels, nous allons créer un nouveau prix pour le lieu. Voici le payload
JSON utilisé :
{
"type": "less_than_12",
"value": 5.75
}
Requête :
http://zestedesavoir.com/media/galleries/3183/
Réponse :
{
"error": {
"code": 500,
"message": "Internal Server Error",
"exception": [
{
"message":
"A circular reference has been detected (configured limit: 1).",
"class":
"Symfony\\Component\\Serializer\\Exception\\CircularReferenceExcept
"trace": [ "..." ]
}
]
}
}
×
Nous obtenons une belle erreur interne ! Pourquoi une exception avec comme message A
circular reference has been detected (configured limit: 1). ?
102
II. Développement de l’API REST
{
"id": 1,
"type": "less_than_12",
"value": 5.75,
"place": {
"..." : "..."
}
}
Les choses se gâtent lorsque le sérialiseur va essayer de transformer le lieu contenu dans notre
objet. Ce lieu contient lui-même l’objet prix qui devra être sérialisé à nouveau. Et la boucle se
répète à l’infini.
http://zestedesavoir.com/media/galleries/3183/
103
II. Développement de l’API REST
— et son adresse.
Le champ prices doit être ignoré.
Tous ces attributs peuvent représenter un groupe : price. À chaque fois que le sérialiseur est
utilisé en spécifiant le groupe price alors seul ces attributs seront sérialisés.
De la même façon, lorsque nous voudrons afficher un lieu, tous les attributs seront affichés en
excluant un seul attribut : le champ place de l’objet Price.
La configuration Symfony pour obtenir un tel comportement est assez simple :
# src/AppBundle/Resources/config/serialization.yml
AppBundle\Entity\Place:
attributes:
id:
groups: ['place', 'price']
name:
groups: ['place', 'price']
address:
groups: ['place', 'price']
prices:
groups: ['place']
AppBundle\Entity\Price:
attributes:
id:
groups: ['place', 'price']
type:
groups: ['place', 'price']
value:
groups: ['place', 'price']
place:
groups: ['price']
i
Il est aussi possible de déclarer les règles de sérialisations avec des annotations sur nos
entités. Pour en savoir plus, il est préférable de consulter la documentation officielle . Les
fichiers de configuration peuvent aussi être placés dans un dossier src/AppBundle/Re-
sources/config/serialization/ afin de mieux les isoler.
104
II. Développement de l’API REST
Pour l’utiliser dans notre contrôleur avec FOSRestBundle, la modification à faire est très simple.
Il suffit d’utiliser l’attribut serializerGroups de l’annotation View.
# src/AppBundle/Controller/Place/PriceController.php
<?php
namespace AppBundle\Controller\Place;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PriceType;
use AppBundle\Entity\Price;
/**
* @Rest\View(serializerGroups={"price"})
* @Rest\Get("/places/{id}/prices")
*/
public function getPricesAction(Request $request)
{
// ...
}
/**
* @Rest\View(statusCode=Response::HTTP_CREATED, serializerGroups={"price"}
* @Rest\Post("/places/{id}/prices")
*/
public function postPricesAction(Request $request)
{
// ...
}
105
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Figure 8.4. – Requête Postman pour récupérer les prix d’un lieu
La réponse ne contient que les attributs que nous avons affectés au groupe price.
[
{
"id": 1,
"type": "less_than_12",
"value": 5.75,
"place": {
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris"
}
}
]
De la même façon, nous devons modifier le contrôleur des lieux pour définir le ou les groupes à
utiliser pour la sérialisation des réponses.
# src/AppBundle/Controller/PlaceController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
*/
public function getPlacesAction(Request $request)
106
II. Développement de l’API REST
{
// ...
}
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places/{id}")
*/
public function getPlaceAction(Request $request)
{
// ...
}
/**
* @Rest\View(statusCode=Response::HTTP_CREATED, serializerGroups={"place"}
* @Rest\Post("/places")
*/
public function postPlacesAction(Request $request)
{
// ...
}
/**
* @Rest\View(statusCode=Response::HTTP_NO_CONTENT, serializerGroups={"plac
* @Rest\Delete("/places/{id}")
*/
public function removePlaceAction(Request $request)
{
// ...
}
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Put("/places/{id}")
*/
public function updatePlaceAction(Request $request)
{
// ...
}
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Patch("/places/{id}")
*/
public function patchPlaceAction(Request $request)
{
// ...
}
// ...
107
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
{
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris",
"prices": [
{
"id": 1,
"type": "less_than_12",
"value": 5.75
}
]
}
Grâce aux groupes, les références circulaires ne sont plus qu’un mauvais souvenir.
!
Les groupes du sérialiseur de Symfony ne sont supportés que depuis la version 2.0 de
FOSRestBundle. Dans le cas où vous utilisez une version de FOSRestBundle inférieure à
la 2.0, il faudra alors utiliser le JMSSerializerBundle à la place du sérialiseur de base
de Symfony.
108
II. Développement de l’API REST
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
/**
* @Rest\View(statusCode=Response::HTTP_NO_CONTENT, serializerGroups={"plac
* @Rest\Delete("/places/{id}")
*/
public function removePlaceAction(Request $request)
{
$em = $this->get('doctrine.orm.entity_manager');
$place = $em->getRepository('AppBundle:Place')
->find($request->get('id'));
/* @var $place Place */
if (!$place) {
return;
}
Avec ce chapitre, nous venons de faire un tour complet des concepts de base pour développer
une API RESTful. Les possibilités d’évolution de notre API sont nombreuses et ne dépendent
que de notre imagination.
Maintenant que les sous ressources n’ont plus de secrets pour nous, nous allons implémenter la
fonctionnalité de base de notre API : Proposer une idée de sortie à un utilisateur.
109
9. TP : Le clou du spectacle - Proposer des
suggestions aux utilisateurs
Nous allons dans cette partie finaliser notre API en rajoutant un système de suggestion pour les
utilisateurs. Tous les concepts de base du style d’architecture qu’est REST ont déjà été abordés.
L’objectif est donc de mettre en pratique les connaissances acquises.
9.1. Énoncé
Afin de gérer les suggestions, nous partons sur un design simple. Dans l’application, nous aurons
une notion de préférences et de thèmes. Chaque utilisateur pourra choisir un ou plusieurs
préférences avec une note sur 10. Et de la même façon, un lieu sera lié à un ou plusieurs thèmes
avec une note sur 10.
Un lieu sera suggéré à un utilisateur si au moins une préférence de l’utilisateur correspond à un
des thèmes du lieu et que le niveau de correspondance est supérieur ou égale à 25.
i
Le niveau de correspondance est une valeur calculée qui nous permettra de quantifier
à quel point un lieu pourrait intéresser un utilisateur. La méthode de calcul est
détaillée ci-dessous.
Pour un utilisateur donné, il faut d’abord prendre toutes ses préférences. Ensuite pour chaque
lieu enregistré dans l’application, si une des préférences de l’utilisateur correspond au thème
du lieu, il faudra calculer le produit : valeur de la préférence de l’utilisateur * valeur du
thème du lieu.
La somme de tous ces produits représente le niveau de correspondance pour ce lieu .
Un exemple vaut mieux que mille discours : Un utilisateur a comme préférences (art, 5), (history,
8) et (architecture, 2). Un lieu 1 a comme thèmes (architecture, 3), (sport, 2), (history, 3). et
un lieu 2 a comme thèmes (art, 3), (science-fiction, 2).
Pour le lieu 1, nous avons 2 thèmes qui correspondent à ses préférences : history et architecture.
history architecture
utilisateur 8 2
lieu 1 4 3
110
II. Développement de l’API REST
8 ∗ 4 + 2 ∗ 3 = 32 + 6 = 38
art
utilisateur 5
lieu 2 3
5 ∗ 3 = 15
111
II. Développement de l’API REST
Une préférence associée à un utilisateur doit avoir une relation bidirectionnelle avec cet utilisateur
et idem pour les lieux.
Une même préférence ne peut pas être associée deux fois à un même utilisateur ou un même
lieu. (ex : un utilisateur ne peut pas avoir 2 fois la préférence art) et idem pour les lieux.
Il faudra 2 tables (donc 2 entités distinctes) :
— preferences (entité Preference) pour stocker les préférences utilisateurs ;
— themes (entité Theme) pour stocker les thèmes sur les lieux.
Il faudra 3 appels API :
— un permettant d’ajouter une préférence pour un utilisateur avec sa valeur ;
— un permettant d’ajouter un thème à un lieu avec sa valeur ;
— un pour récupérer les suggestions d’un utilisateur.
i
Une ressource REST n’est pas forcément une entité brute de notre modèle de données.
Nous pouvons utiliser un appel GET sur l’URL rest-api.local/users/1/suggestions pour
récupérer la liste des suggestions pour l’utilisateur 1.
Une fois que les préférences et les thèmes seront rajoutés, les appels de listing des utilisateurs et
des lieux doivent remonter respectivement les informations sur les préférences et les informations
sur les thèmes. Il faudra donc penser à gérer les références circulaires.
À vous de jouer !
Nous allons commencer notre implémentation en mettant en place la gestion des thèmes.
L’entité contiendra les champs cités plus haut avec en plus une contrainte d’unicité sur le nom
d’un thème et l’identifiant du lieu.
<?php
# src/AppBundle/Entity/Theme.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="themes",
* uniqueConstraints={@ORM\UniqueConstraint(name="themes_name_place_unique
* )
112
II. Développement de l’API REST
*/
class Theme
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="integer")
*/
protected $value;
/**
* @ORM\ManyToOne(targetEntity="Place", inversedBy="themes")
* @var Place
*/
protected $place;
113
II. Développement de l’API REST
L’entité Place doit aussi être modifiée pour avoir une relation bidirectionnelle.
<?php
# src/AppBundle/Entity/Place.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="places",
* uniqueConstraints={@ORM\UniqueConstraint(name="places_name_unique",colu
* )
*/
class Place
{
// ...
/**
* @ORM\OneToMany(targetEntity="Theme", mappedBy="place")
* @var Theme[]
*/
protected $themes;
114
II. Développement de l’API REST
// ...
Pour supporter la création de thèmes pour les lieux, nous allons créer un formulaire Symfony et
les régles de validation associées.
<?php
# src/AppBundle/Form/Type/ThemeType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
i
La liste des thèmes prédéfinis est utilisée pour valider le formulaire Symfony.
115
II. Développement de l’API REST
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Theme:
properties:
name:
- NotNull: ~
- Choice:
choices: [art, architecture, history,
science-fiction, sport]
value:
- NotNull: ~
- Type: numeric
- GreaterThan:
value: 0
- LessThanOrEqual:
value: 10
Pour ajouter un thème, nous allons créer un nouveau contrôleur qui ressemble à quelques lignes
près à ce que nous avons déjà fait jusqu’ici. Nous allons en profiter pour ajouter une méthode
pour lister les thèmes d’un lieu donné.
<?php
# src/AppBundle/Controller/Place/ThemeController.php
namespace AppBundle\Controller\Place;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\ThemeType;
use AppBundle\Entity\Theme;
/**
* @Rest\View(serializerGroups={"theme"})
* @Rest\Get("/places/{id}/themes")
*/
public function getThemesAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id'));
116
II. Développement de l’API REST
if (empty($place)) {
return $this->placeNotFound();
}
return $place->getThemes();
}
/**
* @Rest\View(statusCode=Response::HTTP_CREATED, serializerGroups={"theme"}
* @Rest\Post("/places/{id}/themes")
*/
public function postThemesAction(Request $request)
{
$place = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->find($request->get('id'));
/* @var $place Place */
if (empty($place)) {
return $this->placeNotFound();
}
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($theme);
$em->flush();
return $theme;
} else {
return $form;
}
}
Le fichier de routage de l’application doit être modifié en conséquence pour charger ce nouveau
contrôleur.
117
II. Développement de l’API REST
# app/config/routing.yml
# ...
themes:
type: rest
resource: AppBundle\Controller\Place\ThemeController
# ...
Il ne faut pas oublier de rajouter un nouveau groupe de sérialisation pour la gestion de ces
thèmes.
# src/AppBundle/Resources/config/serialization.yml
AppBundle\Entity\Place:
attributes:
id:
groups: ['place', 'price', 'theme']
name:
groups: ['place', 'price', 'theme']
address:
groups: ['place', 'price', 'theme']
prices:
groups: ['place']
themes:
groups: ['place']
# ...
AppBundle\Entity\Theme:
attributes:
id:
groups: ['place', 'theme']
name:
groups: ['place', 'theme']
value:
groups: ['place', 'theme']
place:
groups: ['theme']
i
Le nouveau groupe est aussi utilisé pour configurer la sérialisation de l’entité Placeafin
d’éviter les références circulaires.
118
II. Développement de l’API REST
Pour la gestion des utilisateurs, nous allons suivre exactement le même schéma d’implémentation.
Les extraits de code fournis se passeront donc de commentaires.
Commençons par l’entité pour la gestion des préférences et le formulaire permettant de le
gérer.
<?php
# src/AppBundle/Entity/Preference.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="preferences",
* uniqueConstraints={@ORM\UniqueConstraint(name="preferences_name_user_un
* )
*/
class Preference
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="integer")
*/
protected $value;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="preferences")
* @var User
*/
protected $user;
119
II. Développement de l’API REST
<?php
# src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
120
II. Développement de l’API REST
* @ORM\Table(name="users",
* uniqueConstraints={@ORM\UniqueConstraint(name="users_email_unique",colum
* )
*/
class User
{
// ...
/**
* @ORM\OneToMany(targetEntity="Preference", mappedBy="user")
* @var Preference[]
*/
protected $preferences;
// ...
Le formulaire associé et les règles de validation sont proches de celui des thèmes.
<?php
# src/AppBundle/Form/Type/PreferenceType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
121
II. Développement de l’API REST
$builder->add('name');
$builder->add('value');
}
# src/AppBundle/Resources/config/validation.yml
# ...
AppBundle\Entity\Preference:
properties:
name:
- NotNull: ~
- Choice:
choices: [art, architecture, history,
science-fiction, sport]
value:
- NotNull: ~
- Type: numeric
- GreaterThan:
value: 0
- LessThanOrEqual:
value: 10
Un nouveau contrôleur sera aussi créé pour assurer la gestion des préférences utilisateurs via
notre API.
<?php
# src/AppBundle/Controller/User/PreferenceController.php
namespace AppBundle\Controller\User;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
122
II. Développement de l’API REST
use AppBundle\Form\Type\PreferenceType;
use AppBundle\Entity\Preference;
/**
* @Rest\View(serializerGroups={"preference"})
* @Rest\Get("/users/{id}/preferences")
*/
public function getPreferencesAction(Request $request)
{
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->find($request->get('id'));
/* @var $user User */
if (empty($user)) {
return $this->userNotFound();
}
return $user->getPreferences();
}
/**
* @Rest\View(statusCode=Response::HTTP_CREATED, serializerGroups={"prefere
* @Rest\Post("/users/{id}/preferences")
*/
public function postPreferencesAction(Request $request)
{
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->find($request->get('id'));
/* @var $user User */
if (empty($user)) {
return $this->userNotFound();
}
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($preference);
123
II. Développement de l’API REST
$em->flush();
return $preference;
} else {
return $form;
}
}
# app/config/routing.yml
# ...
preferences:
type: rest
resource: AppBundle\Controller\User\PreferenceController
Les groupes de sérialisation doivent aussi être mis à jour afin d’éviter les fameuses références
circulaires.
Avec ces modifications que nous venons d’apporter, nous pouvons maintenant associer des
thèmes et des préférences respectivement aux lieux et aux utilisateurs. Nous allons donc finaliser
ce chapitre en rajoutant enfin les suggestions.
i
La technique utilisée pour trouver les suggestions n’est pas optimale. L’objectif ici est
juste de présenter une méthode fonctionnelle et avoir une API complète.
L’algorithme pour calculer le niveau de correspondance va être implémenté dans l’entité User.
À partir des thèmes d’un lieu, nous allons créer une méthode permettant de déterminer le niveau
de correspondance (défini plus haut).
124
II. Développement de l’API REST
<?php
# src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="users",
* uniqueConstraints={@ORM\UniqueConstraint(name="users_email_unique",colum
* )
*/
class User
{
const MATCH_VALUE_THRESHOLD = 25;
// ...
La méthode match de l’objet Preference permet juste de vérifier si le nom du thème est le
même que celui de la préférence de l’utilisateur.
<?php
# src/AppBundle/Entity/Preference.php
namespace AppBundle\Entity;
/**
125
II. Développement de l’API REST
* @ORM\Entity()
* @ORM\Table(name="preferences",
* uniqueConstraints={@ORM\UniqueConstraint(name="preferences_name_user_un
* )
*/
class Preference
{
// ...
Pour récupérer les suggestions, il nous suffit maintenant de créer un appel dans le contrôleur
UserController.
<?php
# src/AppBundle/Controller/UserController.php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\UserType;
use AppBundle\Entity\User;
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/users/{id}/suggestions")
*/
public function getUserSuggestionsAction(Request $request)
{
126
II. Développement de l’API REST
$user = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:User')
->find($request->get('id'));
/* @var $user User */
if (empty($user)) {
return $this->userNotFound();
}
$suggestions = [];
$places = $this->get('doctrine.orm.entity_manager')
->getRepository('AppBundle:Place')
->findAll();
return $suggestions;
}
// ...
!
Un fait important à relever ici est que la méthode, bien qu’étant dans le contrôleur des
utilisateurs, renvoie des lieux. Le groupe de sérialisation utilisé est donc place.
{
"id": 1,
"firstname": "My",
"lastname": "Bis",
"email": "[email protected]",
"preferences": [
{
"id": 1,
127
II. Développement de l’API REST
"name": "history",
"value": 4
},
{
"id": 2,
"name": "art",
"value": 4
},
{
"id": 6,
"name": "sport",
"value": 3
}
]
}
[
{
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris",
"prices": [
{
"id": 1,
"type": "less_than_12",
"value": 5.75
}
],
"themes": [
{
"id": 1,
"name": "architecture",
"value": 7
},
{
"id": 2,
"name": "history",
"value": 6
}
]
},
{
"id": 2,
"name": "Mont-Saint-Michel",
"address": "50170 Le Mont-Saint-Michel",
"prices": [],
128
II. Développement de l’API REST
"themes": [
{
"id": 3,
"name": "history",
"value": 3
},
{
"id": 4,
"name": "art",
"value": 7
}
]
}
]
Quand nous récupérons les suggestions pour notre utilisateur, nous obtenons :
http://zestedesavoir.com/media/galleries/3183/
[
{
"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
}
]
}
]
129
II. Développement de l’API REST
Nous avons donc un lieu dans notre application qui correspondrait aux gouts de notre utilisa-
teur.
Les fonctionnalités que nous voulons pour notre application peuvent être implémentées assez
facilement sans se soucier des contraintes imposées par le style d’architecture REST. REST
n’intervient que pour définir l’API à utiliser pour accéder à ces fonctionnalités et nous laisse
donc la responsabilité des choix techniques et de conceptions.
Vous pouvez vous entrainer et améliorer l’API en rajoutant encore plus de fonctionnalités. Nous
pouvons par exemple imaginer que chaque utilisateur à un budget et que les tarifs des lieux
sont pris en compte pour améliorer les suggestions.
130
10. REST à son paroxysme
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) ?
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 :
<user>
<firstname>test</firstname>
<lastname>XML</lastname>
<email>[email protected]</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
i
C’est au client de définir dans sa requête le format utilisé pour que le serveur puisse la
traiter correctement.
131
II. Développement de l’API REST
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.
http://zestedesavoir.com/media/galleries/3183/
http://zestedesavoir.com/media/galleries/3183/
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.
i
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 .
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.
132
II. Développement de l’API REST
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 per-
mettra 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) :
# 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é priori
ties) 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.
# app/config/config.yml
# ...
fos_rest:
routing_loader:
include_format: false
view:
133
II. Développement de l’API REST
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
i
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.
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é.
i
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.
134
II. Développement de l’API REST
http://zestedesavoir.com/media/galleries/3183/
La réponse est bien en XML et nous pouvons tester avec n’importe quelle méthode de notre
API.
<?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>
135
II. Développement de l’API REST
<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>
http://zestedesavoir.com/media/galleries/3183/
!
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.
136
II. Développement de l’API REST
10.2. 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 ).
{
"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.
137
II. Développement de l’API REST
138
Troisième partie
139
11. Sécurisation de l’API 1/2
Jusque-là, les actions disponibles dans notre API sont accessibles pour n’importe quel client.
Nous ne disposons d’aucun moyen pour gérer l’identité de ces derniers.
Pour être bref, n’importe qui peut faire n’importe quoi avec notre API.
i
La sécurité n’est pas un sujet adressé par les concepts REST mais nous pouvons adapter
les méthodes d’autorisation et d’authentification classiques aux principes REST.
Il existe beaucoup de techniques et d’outils comme OAuth ou JSON Web Tokens permettant
de mettre en place un système d’authentification.
Cependant nous ne nous baserons sur aucun de ces outils et nous allons mettre en place un
système d’authentification totalement personnalisé.
?
Comment mettre en place un tel système en se basant sur des concepts REST ?
Pour bien adapter ses opérations, il faut d’abord bien les comprendre.
En général, lorsque nous nous connectons à un site web, nous fournissons un login et un mot
de passe via un formulaire de connexion. Si les informations fournies sont valides, le serveur
crée un cookie qui permettra d’assurer la gestion de la session. Une fois que nous avons fini de
naviguer sur le site, il suffit de nous déconnecter pour que le cookie de session soit supprimé.
http://zestedesavoir.com/media/galleries/3183/
140
III. Amélioration de l’API REST
Du moment où vous devez interagir avec une entité de votre application, créer une entité, la
modifier, la consulter ou que vous devez l’identifier de manière unique alors vous avez pour
la plupart des cas une ressource.
Les opérations se font sur le cookie, nous pouvons donc dire qu’il représente notre ressource.
Pour le cas d’un site web, l’utilisation d’un cookie est pratique vue que les navigateurs le gèrent
nativement (envoie à chaque requête, limitation à un seul domaine pour la sécurité, durée de
validité, etc.).
Pour le cas d’une API, il est certes possible d’utiliser un cookie mais il existe une solution
équivalente mais plus simple et plus courante : les tokens.
i
Donc se connecter ou encore se déconnecter se traduisent respectivement par créer un
token d’authentification et supprimer son token d’authentification.
Pour chaque requête, le token ainsi crée est rajouté en utilisant une entête HTTP comme pour
les cookies.
Commençons d’abord par gérer la création des tokens.
# src/AppBundle/Entity/User.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="users",
* uniqueConstraints={@ORM\UniqueConstraint(name="users_email_unique",colum
* )
*/
141
III. Amélioration de l’API REST
class User
{
//...
/**
* @ORM\Column(type="string")
*/
protected $password;
protected $plainPassword;
# src/AppBundle/Entity/User.php
<?php
142
III. Amélioration de l’API REST
namespace AppBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity()
* @ORM\Table(name="users",
* uniqueConstraints={@ORM\UniqueConstraint(name="users_email_unique",colum
* )
*/
class User implements UserInterface
{
// ...
Le formulaire de création d’utilisateur et l’action associée dans notre contrôleur vont être adaptés
143
III. Amélioration de l’API REST
en conséquence :
# src/AppBundle/Form/Type/UserType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
// ...
}
Pour le mot de passe, nous aurons juste quelques règles de validation basiques :
# src/AppBundle/Resources/config/validation.yml
# ...
AppBundle\Entity\User:
constraints:
-
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
email
properties:
firstname:
- NotBlank: ~
- Type: string
lastname:
- NotBlank: ~
- Type: string
email:
- NotBlank: ~
- Email: ~
plainPassword:
144
III. Amélioration de l’API REST
Le champ plainPassword est un champ un peu spécial. Les groupes nous permettrons d’activer
sa contrainte NotBlank lorsque le client voudra créer ou mettre à jour tous les champs de
l’utilisateur. Mais lors d’une mise à jour partielle (PATCH), si le champ est nul, il sera tout
simplement ignoré.
×
Le mot de passe ne doit en aucun cas être sérialisé. Il ne doit pas être associé à un groupe
de sérialisation.
# src/AppBundle/Controller/UserController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\UserType;
use AppBundle\Entity\User;
145
III. Amélioration de l’API REST
$form->submit($request->request->all());
if ($form->isValid()) {
$encoder = $this->get('security.password_encoder');
// le mot de passe en claire est encodé avant la
sauvegarde
$encoded = $encoder->encodePassword($user,
$user->getPlainPassword());
$user->setPassword($encoded);
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($user);
$em->flush();
return $user;
} else {
return $form;
}
}
/**
* @Rest\View(serializerGroups={"user"})
* @Rest\Put("/users/{id}")
*/
public function updateUserAction(Request $request)
{
return $this->updateUser($request, true);
}
/**
* @Rest\View(serializerGroups={"user"})
* @Rest\Patch("/users/{id}")
*/
public function patchUserAction(Request $request)
{
return $this->updateUser($request, false);
}
if (empty($user)) {
return $this->userNotFound();
}
146
III. Amélioration de l’API REST
$form->submit($request->request->all(), $clearMissing);
if ($form->isValid()) {
// Si l'utilisateur veut changer son mot de passe
if (!empty($user->getPlainPassword())) {
$encoder = $this->get('security.password_encoder');
$encoded = $encoder->encodePassword($user,
$user->getPlainPassword());
$user->setPassword($encoded);
}
$em = $this->get('doctrine.orm.entity_manager');
$em->merge($user);
$em->flush();
return $user;
} else {
return $form;
}
}
Le groupe de validation Default regroupe toutes les contraintes de validation qui ne sont dans
aucun groupe. Il est créé automatiquement par Symfony. N’hésitez surtout pas à consulter la
documentation pour des informations plus détaillées avant de continuer.
Nous pouvons maintenant tester la création d’un utilisateur en fournissant un mot de passe.
http://zestedesavoir.com/media/galleries/3183/
147
III. Amélioration de l’API REST
{
"id": 5,
"firstname": "test",
"lastname": "Pass",
"email": "[email protected]",
// ...
}
!
Toutes les modifications effectuées ici sont propres à Symfony. Si vous avez du mal à
suivre, il est vivement (grandement) conseillé de consulter la documentation officielle
du framework.
http://zestedesavoir.com/media/galleries/3183/
148
III. Amélioration de l’API REST
# src/AppBundle/Entity/AuthToken.php
<?php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="auth_tokens",
* uniqueConstraints={@ORM\UniqueConstraint(name="auth_tokens_value_unique
* )
*/
class AuthToken
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $value;
/**
* @ORM\Column(type="datetime")
* @var \DateTime
*/
protected $createdAt;
/**
* @ORM\ManyToOne(targetEntity="User")
* @var User
*/
protected $user;
149
III. Amélioration de l’API REST
150
III. Amélioration de l’API REST
— une entité nommée Credentials avec deux attributs : login et password. Cette entité
n’aura aucune annotation Doctrine, elle pemettra juste de transporter ces informations ;
— un formulaire nommé CredentialsType pour valider que les champs de l’entité Creden
tials ne sont pas vides (Not-Blank).
L’entité ressemble donc à :
# src/AppBundle/Entity/Credentials.php
<?php
namespace AppBundle\Entity;
class Credentials
{
protected $login;
protected $password;
# src/AppBundle/Form/Type/CredentialsType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
151
III. Amélioration de l’API REST
# src/AppBundle/Resources/config/validation.yml
# ...
AppBundle\Entity\Credentials:
properties:
login:
- NotBlank: ~
- Type: string
password:
- NotBlank: ~
- Type: string
Il ne faut pas oublier de configurer le sérialiseur pour afficher le token en utilisant un groupe
prédéfini.
# src/AppBundle/Resources/config/serialization.yml
# ...
AppBundle\Entity\User:
attributes:
id:
groups: ['user', 'preference', 'auth-token']
firstname:
groups: ['user', 'preference', 'auth-token']
lastname:
groups: ['user', 'preference', 'auth-token']
email:
152
III. Amélioration de l’API REST
AppBundle\Entity\AuthToken:
attributes:
id:
groups: ['auth-token']
value:
groups: ['auth-token']
createdAt:
groups: ['auth-token']
user:
groups: ['auth-token']
Maintenant, il ne reste plus qu’à créer le contrôleur qui assure la gestion des tokens d’authentifi-
cation.
# src/AppBundle/Controller/AuthTokenController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\CredentialsType;
use AppBundle\Entity\AuthToken;
use AppBundle\Entity\Credentials;
$form->submit($request->request->all());
if (!$form->isValid()) {
return $form;
153
III. Amélioration de l’API REST
$em = $this->get('doctrine.orm.entity_manager');
$user = $em->getRepository('AppBundle:User')
->findOneByEmail($credentials->getLogin());
$encoder = $this->get('security.password_encoder');
$isPasswordValid = $encoder->isPasswordValid($user,
$credentials->getPassword());
$em->persist($authToken);
$em->flush();
return $authToken;
}
# app/config/routing.yml
# ...
auth-tokens:
type: rest
resource: AppBundle\Controller\AuthTokenController
154
III. Amélioration de l’API REST
i
Pour des raisons de sécurité, nous évitons de donner des détails sur les comptes existants,
un même message - Invalid Credentials - est renvoyé lorsque le login n’existe pas ou
lorsque le mot de passe n’est pas correct.
Nous pouvons maintenant créer un token en utilisant le compte [email protected] créé plus tôt.
{
"login": "[email protected]",
"password": "test"
}
http://zestedesavoir.com/media/galleries/3183/
La réponse contient un token que nous pourrons exploiter plus tard pour décliner notre identité.
{
"id": 3,
"value":
"MVgq3dT8QyWv3t+s7DLyvsquVbu+mOSPMdYX7VUQOEQcJGwaGD8ETa+zi9ReHPWYFKI=",
"createdAt": "2016-04-08T17:49:00+00:00",
"user": {
"id": 5,
"firstname": "test",
"lastname": "Pass",
"email": "[email protected]"
}
}
Nous disposons maintenant d’un système fonctionnel pour générer des tokens pour les utilisateurs.
Ces tokens nous permettrons par la suite de vérifier l’identité des clients de l’API afin de la
sécuriser.
155
12. Sécurisation de l’API 2/2
Un client utilisant notre API est maintenant en mesure de créer des tokens d’authentification.
Nous allons donc rajouter un système de sécurité afin d’imposer l’utilisation de ce token pour
accéder à notre API REST. Au lieu d’envoyer le login et le mot de passe dans chaque requête,
nous utiliserons le token associé au client.
i
Le nom de notre entête ne vient pas du néant. De manière conventionnelle, lorsqu’une
requête contient une entête n’appartenant pas aux spécifications HTTP, le nom dé-
bute par X-. Ensuite, le reste du nom reflète le contenu de l’entête, Auth-Token pour
Authentication Token.
Nous avons beaucoup d’exemples dans notre API actuelle qui suivent ce modèle de nommage.
Lorsque nous consultons les entêtes d’une réponse quelconque de notre API, nous pouvons voir
X-Debug-Token (créé par Symfony en mode config dev) ou encore X-Powered-By (créé par
PHP).
http://zestedesavoir.com/media/galleries/3183/
Symfony dispose d’un mécanisme spécifique permettant de gérer les clés d’API. Il existe un
cookbook décrivant de manière très succincte les mécanismes en jeu pour le mettre en place.
Pour résumer, à chaque requête de l’utilisateur, un listener est appelé afin de vérifier que la
requête contient une entête nommée X-Auth-Token. Et si tel est le cas, son existence dans
notre base de données et sa validité sont vérifiées.
156
III. Amélioration de l’API REST
!
Pour une requête permettant de créer un token d’authentification, ce listener ne fait
aucune action afin d’autoriser la requête.
http://zestedesavoir.com/media/galleries/3183/
Pour simplifier notre implémentation, nous considérons qu’un token d’authentification est invalide
si son ancienneté est supérieur à 12 heures. Vous pouvez cependant modifier ce comportement
et définir les règles de validité que vous voulez.
http://zestedesavoir.com/media/galleries/3183/
Comme pour tous les systèmes d’authentification de Symfony, nous avons besoin d’un fournisseur
d’utilisateurs (UserProvider). Pour notre cas, il faut que notre fournisseur puisse charger un
token en utilisant la valeur dans notre entête X-Auth-Token.
<?php
# src/AppBundle/Security/AuthTokenUserProvider.php
namespace AppBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use
Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
157
III. Amélioration de l’API REST
Cette classe permettra de récupérer les utilisateurs en se basant sur le token d’authentification
fourni.
Pour piloter le mécanisme d’authentification, nous devons créer une classe implémentant l’inter-
face SimplePreAuthenticatorInterface de Symfony. C’est cette classe qui gère la cinéma-
tique d’authentification que nous avons décrite plus haut.
# src/AppBundle/Security/AuthTokenAuthenticator.php
<?php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use
Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use
Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
158
III. Amélioration de l’API REST
use
Symfony\Component\Security\Core\Exception\AuthenticationException;
use
Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationEx
use
Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use
Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandler
use
Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterf
use Symfony\Component\Security\Http\HttpUtils;
protected $httpUtils;
$targetUrl = '/auth-tokens';
// Si la requête est une création de token, aucune
vérification n'est effectuée
if ($request->getMethod() === "POST" &&
$this->httpUtils->checkRequestPath($request,
$targetUrl)) {
return;
}
$authTokenHeader = $request->headers->get('X-Auth-Token');
if (!$authTokenHeader) {
throw new
BadCredentialsException('X-Auth-Token header is required');
}
159
III. Amélioration de l’API REST
$authTokenHeader,
$providerKey
);
}
$authTokenHeader = $token->getCredentials();
$authToken = $userProvider->getAuthToken($authTokenHeader);
if (!$authToken || !$this->isTokenValid($authToken)) {
throw new
BadCredentialsException('Invalid authentication token');
}
$user = $authToken->getUser();
$pre = new PreAuthenticatedToken(
$user,
$authTokenHeader,
$providerKey,
$user->getRoles()
);
return $pre;
}
/**
* Vérifie la validité du token
160
III. Amélioration de l’API REST
*/
private function isTokenValid($authToken)
{
return (time() -
$authToken->getCreatedAt()->getTimestamp()) <
self::TOKEN_VALIDITY_DURATION;
}
# app/config/services.yml
services:
auth_token_user_provider:
class: AppBundle\Security\AuthTokenUserProvider
arguments: ["@auth_token_repository", "@user_repository"]
public: false
auth_token_repository:
class: Doctrine\ORM\EntityManager
factory: ["@doctrine.orm.entity_manager", "getRepository"]
arguments: ["AppBundle:AuthToken"]
user_repository:
class: Doctrine\ORM\EntityManager
factory: ["@doctrine.orm.entity_manager", "getRepository"]
arguments: ["AppBundle:User"]
auth_token_authenticator:
class: AppBundle\Security\AuthTokenAuthenticator
arguments: ["@security.http_utils"]
public: false
Nous devons maintenant activer le pare-feu (firewall) de Symfony et le configurer avec notre
161
III. Amélioration de l’API REST
# app/config/security.yml
#
http://symfony.com/doc/current/book/security.html#where-do-users-come-f
providers:
auth_token_user_provider:
id: auth_token_user_provider
firewalls:
# disables authentication for assets and the profiler,
adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
stateless: true
simple_preauth:
authenticator: auth_token_authenticator
provider: auth_token_user_provider
anonymous: ~
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
cost: 12
!
Vous pouvez remarquer que le pare-feu (firewall) est configuré en mode stateless. À
chaque requête, l’identité de l’utilisateur est revérifiée. La session n’est jamais utilisée.
Maintenant, lorsque nous essayons de lister les lieux sans mettre l’entête d’authentification, une
exception est levée :
http://zestedesavoir.com/media/galleries/3183/
Figure 12.4. – Requête Postman pour lister les lieux sans token d’authentification
162
III. Amélioration de l’API REST
{
"error": {
"code": 500,
"message": "Internal Server Error",
"exception": [
{
"message": "X-Auth-Token header is required",
"class":
"Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsExcep
"trace": [
"..."
]
}
]
}
}
Spoil ! Les codes de statut et les messages renvoyés pour ce cas de figure ne sont pas conformes
aux principes REST. Nous verrons dans ce chapitre comment corriger le tir.
Pour le moment, l’exception BadCredentialsException, avec le message X-Auth-Token
header is required, confirme bien que la vérification du token est effectuée.
En rajoutant le token que nous avions généré plus tôt, la réponse contient bien la liste des lieux
de notre application.
Avec Postman, il faut accéder à l’onglet Headers en dessous de l’URL pour ajouter des entêtes à
notre requête.
http://zestedesavoir.com/media/galleries/3183/
Figure 12.5. – Requête Postman pour lister les lieux avec un token d’authentification
[
{
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris",
"prices": [
{
"id": 1,
"type": "less_than_12",
"value": 5.75
163
III. Amélioration de l’API REST
}
],
"themes": [
{
"id": 1,
"name": "architecture",
"value": 7
},
{
"id": 2,
"name": "history",
"value": 6
}
]
},
// ...
]
# app/config/config.yml
# ...
fos_rest:
routing_loader:
include_format: false
# ...
exception:
enabled: true
164
III. Amélioration de l’API REST
En activant ce composant, la gestion des exceptions avec Twig est automatiquement désactivée.
Rien qu’avec cette configuration, nous pouvons voir un changement dans la réponse lorsque
l’entête X-Auth-Token n’est pas renseignée.
{
"code": 500,
"message": "X-Auth-Token header is required"
}
http://zestedesavoir.com/media/galleries/3183/
Lorsque le token renseigné n’est pas valide, nous obtenons comme réponse :
{
"code": 500,
"message": "Invalid authentication token"
}
Les messages correspondent à ceux que nous avons défini dans les exceptions parce que l’appli-
cation est en mode développement. En production, ces messages sont remplacés par Internal
Server Error. Pour s’en rendre compte, il suffit de lancer la même requête avec comme URL
rest-api.local/app.php/places pour forcer la configuration en production.
{
"code": 500,
"message": "Internal Server Error"
}
Il peut arriver que nous voulions conserver les messages des exceptions même en production.
Pour ce faire, il suffit de rajouter dans la configuration un système d’autorisation des exceptions
concernées.
# app/config/config.yml
# ...
fos_rest:
165
III. Amélioration de l’API REST
routing_loader:
include_format: false
# ...
exception:
enabled: true
messages:
'Symfony\Component\Security\Core\Exception\BadCredentialsExcept
true
Le tableau message contient comme clés les noms des exceptions à autoriser et la valeur vaut
true.
En re-testant, la requête sur l’URL rest-api.local/app.php/places (n’oubliez pas de vider
le cache avant de tester), le message est bien affiché :
{
"code": 500,
"message": "Invalid authentication token"
}
# app/config/config.yml
# ...
fos_rest:
routing_loader:
include_format: false
# ...
exception:
enabled: true
messages:
'Symfony\Component\Security\Core\Exception\BadCredentialsExcept
true
166
III. Amélioration de l’API REST
codes:
'Symfony\Component\Security\Core\Exception\BadCredentialsExcept
401
Encore une fois ce bundle, nous facilite grandement le travail et réduit considérablement le
temps de développement.
Lorsque nous re-exécutons notre requête :
http://zestedesavoir.com/media/galleries/3183/
http://zestedesavoir.com/media/galleries/3183/
{
"code": 401,
"message": "X-Auth-Token header is required"
}
i
L’attribut code dans la réponse est créé par FOSRestBundle par soucis de clarté. La
contrainte REST, elle, exige juste que le code HTTP de la réponse soit conforme.
Vu que le bundle est conçu pour interagir avec Symfony, toutes les exceptions du framework qui
implémentent l’interface Symfony\Component\HttpKernel\Exception\HttpExceptionInterface‘
peuvent être traitées automatiquement.
Si par exemple, nous utilisons l’exception NotFoundHttpException, le code de statut devient
automatiquement 404. En général, il est aussi utile d’autoriser tous les messages des exceptions
de type HttpException pour faciliter la gestion des cas d’erreurs.
La configuration du bundle devient maintenant :
167
III. Amélioration de l’API REST
# app/config/config.yml
# ...
fos_rest:
routing_loader:
include_format: false
# ...
exception:
enabled: true
messages:
'Symfony\Component\HttpKernel\Exception\HttpException'
: true
'Symfony\Component\Security\Core\Exception\BadCredentialsExcept
true
codes:
'Symfony\Component\Security\Core\Exception\BadCredentialsExcept
401
http://zestedesavoir.com/media/galleries/3183/
http://zestedesavoir.com/media/galleries/3183/
168
III. Amélioration de l’API REST
i
En résumé, le code de statut 401 permet de signaler au client qu’il doit décliner son
identité et le code de statut 403 permet de notifier à un client déjà identifié qu’il ne
dispose pas de droits suffisants.
# src/AppBunle/Controller/AuthTokenController.php
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
169
III. Amélioration de l’API REST
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\CredentialsType;
use AppBundle\Entity\AuthToken;
use AppBundle\Entity\Credentials;
/**
* @Rest\View(statusCode=Response::HTTP_NO_CONTENT)
* @Rest\Delete("/auth-tokens/{id}")
*/
public function removeAuthTokenAction(Request $request)
{
$em = $this->get('doctrine.orm.entity_manager');
$authToken = $em->getRepository('AppBundle:AuthToken')
->find($request->get('id'));
/* @var $authToken AuthToken */
$connectedUser = $this-
>get('security.token_storage')->getToken()->getUser();
// ...
}
http://zestedesavoir.com/media/galleries/3183/
Si tout se passe bien, la réponse lors d’une suppression est vide avec comme statut 204. En cas
d’erreur une réponse 400 est renvoyée au client.
170
III. Amélioration de l’API REST
{
"code": 400,
"message": "Bad Request"
}
Notre fameux tableau récapitulatif s’enrichit d’un nouveau code de statut et listing des entêtes
HTTP utilisables :
171
III. Amélioration de l’API REST
meilleure des sécurités ne vaut rien si le protocole utilisé n’est pas sécurisé. Donc dans une API
en production, il faut systématiquement utiliser le HTTPS.
172
13. Créer une ressource avec des relations
Revenons un peu sur les relations entre les ressources.
À la création d’un lieu, nous ne pouvons pas renseigner les tarifs. Nous sommes donc obligés de
créer d’abord un lieu avant de rajouter ses tarifs.
Même si ce fonctionnement pourrait convenir dans certains cas, il peut aussi s’avérer utile de
créer un lieu et de lui associer des tarifs avec un seul appel API. On pourra ainsi optimiser l’API
et réduire les interactions entre le client et le serveur.
Nous allons donc voir dans cette partie comment arriver à un tel résultat avec Symfony.
{
"name": "Disneyland Paris",
"address": "77777 Marne-la-Vallée"
}
En réalité, pour supporter la création d’un lieu avec ses tarifs, les contraintes de REST ne
rentrent pas en jeu. Nous pouvons adapter librement le payload afin de rajouter toutes les
informations nécessaires pour supporter la création d’un lieu avec ses tarifs avec un seul appel.
Vu que nous avons déjà une méthode pour créer des tarifs, nous allons utiliser le même payload
pour la création d’un lieu pour garder une API cohérente. Le payload existant doit être
maintenant :
{
"name": "Disneyland Paris",
"address": "77777 Marne-la-Vallée",
173
III. Amélioration de l’API REST
"prices": [
{
"type": "for_all",
"value": 10.0
},
{
"type": "less_than_12",
"value": 5.75
}
]
}
L’attribut prices est un tableau qui contiendra la liste des prix que nous voulons rajouter à la
création du lieu.
Les tarifs resteront optionnels ce qui nous permettra de créer des lieux avec ou sans. Nous allons
sans plus attendre appliquer ces modifications à l’appel existant.
13.2.2. Implémentation
La méthode pour créer un lieu reste inchangée. Nous devons juste changer le formulaire des
lieux et le traitement associé.
<?php
# src/AppBundle/Form/Type/PlaceType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
174
III. Amélioration de l’API REST
La configuration du formulaire est typique des formulaires Symfony avec une collection. La
documentation officielle aborde le sujet d’une manière plus complète.
Les règles de validation pour les thèmes existent déjà. Pour les utiliser, nous devons modifier la
validation de l’entité Place en rajoutant la règle Valid. Avec cette annotation, nous disons
à Symfony de valider l’attribut prices en utilisant les contraintes de validation de l’entité
Price.
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Place:
constraints:
-
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
name
properties:
name:
- NotBlank: ~
- Type: string
address:
- NotBlank: ~
- Type: string
prices:
- Valid: ~
Notez qu’il n’y a pas d’assertions de type NotBlank puisque l’attribut prices est optionnel.
Avec les modifications que nous venons d’apporter, nous pouvons déjà tester la création d’un
lieu avec des prix. Mais avant de le faire, nous allons rapidement adapter le contrôleur pour
gérer la sauvegarde des prix.
<?php
# src/AppBundle/Controller/PlaceController.php
175
III. Amélioration de l’API REST
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest; // alias pour
toutes les annotations
use AppBundle\Form\Type\PlaceType;
use AppBundle\Entity\Place;
// ...
/**
* @Rest\View(statusCode=Response::HTTP_CREATED, serializerGroups={"place"}
* @Rest\Post("/places")
*/
public function postPlacesAction(Request $request)
{
$place = new Place();
$form = $this->createForm(PlaceType::class, $place);
$form->submit($request->request->all());
if ($form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
foreach ($place->getPrices() as $price) {
$price->setPlace($place);
$em->persist($price);
}
$em->persist($place);
$em->flush();
return $place;
} else {
return $form;
}
}
// ...
!
Comme vous l’avez sûrement remarqué, toute la logique de notre API est regroupée dans
les contrôleurs. Ceci n’est pas une bonne pratique et l’utilisation d’un service dédié est
vivement conseillée pour une application destinée à une mise en production.
176
III. Amélioration de l’API REST
L’entité Place a été légèrement modifiée. L’attribut prices est maintenant initialisé avec une
collection vide.
<?php
# src/AppBundle/Entity/Place.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="places",
* uniqueConstraints={@ORM\UniqueConstraint(name="places_name_unique",colu
* )
*/
class Place
{
// ...
/**
* @ORM\OneToMany(targetEntity="Price", mappedBy="place")
* @var Price[]
*/
protected $prices;
// ...
{
"name": "Musée du Louvre",
"address": "799, rue de Rivoli, 75001 Paris",
"prices": [
{
"type": "less_than_12",
"value": 6
},
{
"type": "for_all",
"value": 15
177
III. Amélioration de l’API REST
}
]
}
La réponse est identique à ce que nous avons déjà eu mais les prix sont enregistrés en même
temps.
{
"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
}
],
178
III. Amélioration de l’API REST
"themes": []
}
Nous pouvons maintenant créer un lieu tout en rajoutant des prix et le principe peut même être
élargi pour les thèmes des lieux et les préférences des utilisateurs.
Si nous essayons de créer un lieu avec des prix du même type, nous obtenons une erreur interne
car il y a une contrainte d’unicité sur l’identifiant du lieu et le type du produit.
<?php
# src/AppBundle/Entiy/Price.php
namespace AppBundle\Entity;
/**
* @ORM\Entity()
* @ORM\Table(name="prices",
* uniqueConstraints={@ORM\UniqueConstraint(name="prices_type_place_unique
* )
*/
class Price
{
// ...
}
Pour s’en convaincre, il suffit d’essayer de créer un nouveau lieu avec comme payload :
{
"name": "Arc de Triomphe",
"address": " Place Charles de Gaulle, 75008 Paris",
"prices": [
{
"type": "less_than_12",
"value": 0.0
},
{
"type": "less_than_12",
179
III. Amélioration de l’API REST
"value": 0.0
}
]
}
{
"code": 500,
"message":
"An exception occurred while executing 'INSERT INTO prices (type, value,
}
Pour corriger le problème, nous allons créer une règle de validation personnalisée.
La contrainte est la partie la plus simple à implémenter. Il suffit d’une classe pour la nommer et
d’un message en cas d’erreur.
<?php
# src/AppBundle/Form/Validator/Constraint/PriceTypeUnique.php
namespace AppBundle\Form\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class PriceTypeUnique extends Constraint
{
public $message =
'A place cannot contain prices with same type';
}
Une fois que nous avons une nouvelle contrainte, il reste à créer un validateur pour gérer cette
contrainte.
180
III. Amélioration de l’API REST
<?php
#
src/AppBundle/Form/Validator/Constraint/PriceTypeUniqueValidator.php
namespace AppBundle\Form\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
$pricesType = [];
Le nom choisi n’est pas un hasard. Vu que la contrainte s’appelle PriceTypeUnique, le valida-
teur a été nommé PriceTypeUniqueValidator afin d’utiliser les conventions de nommage de
Symfony. Ainsi notre contrainte est validée en utilisant le validateur que nous venons de créer.
i
Ce comportement par défaut peut être modifié en étendant la méthode validatedBy de
la contrainte. La documentation officielle de Symfony apporte plus d’informations à ce
sujet.
Pour utiliser notre nouvelle contrainte, nous allons modifier les règles de validation qui s’applique
à un lieu :
181
III. Amélioration de l’API REST
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Place:
constraints:
-
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
name
properties:
name:
- NotBlank: ~
- Type: string
address:
- NotBlank: ~
- Type: string
prices:
- Valid: ~
- AppBundle\Form\Validator\Constraint\PriceTypeUnique:
~
En testant à nouveau la création d’un lieu avec deux prix du même type, nous obtenons une
belle erreur de validation avec un message clair.
{
"code": 400,
"message": "Validation Failed",
"errors": {
"children": {
"name": [],
"address": [],
"prices": {
"errors": [
"A place cannot contain prices with same type"
],
"children": [
{
"children": {
"type": [],
"value": []
}
},
{
"children": {
"type": [],
"value": []
}
}
]
}
182
III. Amélioration de l’API REST
}
}
}
Comme vous avez pu le remarquer, la création d’une ressource en relation avec d’autres ressources
ne relève pas trop de REST mais plutôt de la gestion des formulaires avec Symfony.
Ainsi, les connaissances que vous avez déjà pu acquérir pour la gestion des formulaires dans
Symfony peuvent être exploitées pour mettre en place ces fonctionnalités.
Prendre en compte ce genre de détails d’implémentation permet de réduire le nombre d’appels
API et donc améliorer les performances des applications qui doivent l’exploiter et l’expérience
utilisateur par la même occasion.
183
14. 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.
?
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).
Source : 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.
184
III. Amélioration de l’API REST
Pour accéder à toutes ces fonctionnalités, il suffit d’utiliser une annotation FOS\RestBundle\Control
ler\Annotations\QueryParam sur le ou les actions de nos contrôleurs.
i
Il est aussi possible d’utiliser cette annotation sur un contrôleur mais nous ne parlerons
pas de ce cas d’usage.
<?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.
14.2.2. 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 :
185
III. Amélioration de l’API REST
# 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.
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.
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
use FOS\RestBundle\Controller\Annotations\QueryParam;
186
III. Amélioration de l’API REST
// ...
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
* @QueryParam(name="offset", requirements="\d+", default="", description="
* @QueryParam(name="limit", requirements="\d+", default="", description="I
*/
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\Re
quest\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.
187
III. Amélioration de l’API REST
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Request\ParamFetcher;
// ...
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
* @QueryParam(name="offset", requirements="\d+", default="", description="
* @QueryParam(name="limit", requirements="\d+", default="", description="N
*/
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.
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Request\ParamFetcher;
188
III. Amélioration de l’API REST
// ...
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
* @QueryParam(name="offset", requirements="\d+", default="", description="
* @QueryParam(name="limit", requirements="\d+", default="", description="N
*/
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;
}
// ...
}
189
III. Amélioration de l’API REST
http://zestedesavoir.com/media/galleries/3183/
[
{
"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": []
}
]
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.
<?php
# src/AppBundle/Controller/PlaceController.php
190
III. Amélioration de l’API REST
namespace AppBundle\Controller;
// ...
use FOS\RestBundle\Controller\Annotations\QueryParam;
// ...
/**
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
* @QueryParam(name="offset", requirements="\d+", default="", description="
* @QueryParam(name="limit", requirements="\d+", default="", description="N
* @QueryParam(name="sort", requirements="(asc|desc)", nullable=true, descr
*/
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);
}
$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 à
191
III. Amélioration de l’API REST
http://zestedesavoir.com/media/galleries/3183/
Figure 14.2. – Récupération des lieux avec une pagination et un tri par ordre décroissant de
nom
La réponse change en :
[
{
"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 .
192
III. Amélioration de l’API REST
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.
193
15. JMSSerializer : Une alternative au
sérialiseur natif de Symfony
Le sérialiseur natif de Symfony est disponible depuis les toutes premières versions du framework.
Cependant, les fonctionnalités supportées par celui-ci étaient assez basique.
Par exemple, les groupes de sérialisation - permettant entre autres de gérer les références
circulaires - n’ont été supportés qu’à partir de la version 2.7 sortie en 2015 . La sérialisation
des dates PHP (DateTime et DateTimeImmutable) n’a été supporté qu’avec la version 3.1 sortie
en 2016 .
Pour pallier à ce retard, un bundle a été développé pour la gestion de la sérialisation dans
Symfony : JMSSerializerBundle. Il permet d’intégrer la librairie JMSSerializer et est très
largement utilisé dans le cadre du développement d’une API avec Symfony.
194
III. Amélioration de l’API REST
Comme pour tous les bundles de Symfony, il suffit de le télécharger avec Composer et de l’activer.
Téléchargement du bundle :
Activation du bundle :
<?php
# app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
La configuration par défaut de ce bundle suffit largement pour commencer à l’exploiter. Mais
pour notre cas, puisque nous avons déjà pas mal de fonctionnalités qui dépendent du sérialiseur,
nous allons modifier sa configuration.
195
III. Amélioration de l’API REST
Comme pour le sérialiseur natif de Symfony (depuis la version 3.1), la sérialisation des dates dans
php est supportée nativement par JMSSerializerBundle. Nous pouvons, en plus, personnaliser
ce comportement avec juste 4 lignes de configuration.
# app/config/config.yml
# ...
jms_serializer:
handlers:
datetime:
default_format: "Y-m-d\\TH:i:sP"
default_timezone: "UTC"
i
L’attribut default_format prend en paramètre le même format que la fonction date de
PHP .
Dans tous les exemples que nous avons pu voir, les attributs dans les requêtes et les réponses
sont toutes en minuscules. À part l’attribut plainPassword utilisé pour créer un utilisateur
et le champ createdAt associé à un token d’authentification, toutes nos attributs sont en
minuscule. Mais dans le cadre d’une API plus complète, la question de la casse va se poser.
La seule contrainte qu’il faudra garder en tête est la cohérence. Si nous décidons d’utiliser des
noms d’attributs en camelCase ou en snake_case, il faudra s’en tenir à ça pour tous les appels
de l’API.
La configuration de tels paramètres est très simple aussi bien avec le sérialiseur de base de
Symfony qu’avec le JMSSerializer. Nous allons donc garder la configuration par défaut du
sérialiseur de Symfony qui est de conserver le même nom que celui des attributs de nos objets.
# app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
parameters:
locale: en
196
III. Amélioration de l’API REST
jms_serializer.camel_case_naming_strategy.class:
JMS\Serializer\Naming\IdenticalPropertyNamingStrategy
# ...
Maintenant que nous avons fini la configuration, il faut désactiver le sérialiseur natif de Sym-
fony.
Vu qu’il n’est pas activé par défaut, nous pouvons retirer la configuration associée ou passer sa
valeur à false.
# app/config/config.yml
# ...
framework:
# ...
serializer:
enabled: false
# ...
Le comportement par défaut de JMSSerializer est d’ignorer tous les attributs nuls d’un objet.
Ce fonctionnement peut entrainer des réponses avec des payloads partiels manquant certains
attributs. Pour éviter ce problème, FOSRestBundle propose un paramètre de configuration pour
forcer JMSSerializer à sérialiser les attributs nuls.
# app/config/config.yml
# ...
fos_rest:
serializer:
serialize_null: true
197
III. Amélioration de l’API REST
Pour tester notre configuration, nous allons lister les lieux dans notre application.
La réponse obtenue est :
{
"0": {},
"1": {}
}
Nous avons là une bonne et une mauvaise nouvelle. Le bundle est bien utilisé pour sérialiser la
réponse mais les groupes de sérialisation, que nous avons définis, ne sont pas encore exploités.
?
Pourquoi la réponse n’est pas sérialisée correctement ?
Le sérialiseur est pleinement supporté par FOSRestBundle. Les configurations dans tous nos
contrôleurs sont déjà compatibles. Par contre, le fichier src/AppBundle/Resources/config/se-
rialization.yml décrivant les règles de sérialisation, est ignoré par JMSSerializerBundle.
La configuration par défaut se base sur une convention simple. Pour un bundle, les fichiers décri-
vant la sérialisation doivent être dans le dossier src/NomDuBundle/Resources/config/se-
rializer/ .
Le nom de chaque fichier contenant les règles de sérialisation d’une classe est obtenu en faisant
deux opérations :
— le nom du bundle est retiré du namespace (espace de nom) de la classe ;
— les séparateurs anti-slash (\) sont remplacés par des points (.) ;
— et enfin, l’extension yml ou xml est rajouté au nom ainsi obtenu.
Par exemple, pour la classe NomDuBundle\A\B, si nous voulons utiliser une configuration
en YAML, nous devons avoir un fichier src/NomDuBundle/Resources/config/seriali-
zer/A.B.yml.
i
JMSSerialiserBundle supporte aussi les annotations et les fichiers XML pour la configura-
tion des règles de sérialisation. D’ailleurs, si nous avions utilisé les annotations, le code
fonctionnerait sans adaptation de notre part.
198
III. Amélioration de l’API REST
Pour remettre notre API d’aplomb, nous allons créer les fichiers de configuration pour les classes
utilisées.
Commençons par l’entité Place. La configuration pour cette classe devient :
# src/AppBundle/Resources/config/serializer/Entity.Place.yml
AppBundle\Entity\Place:
exclusion_policy: none
properties:
id:
groups: ['place', 'price', 'theme']
name:
groups: ['place', 'price', 'theme']
address:
groups: ['place', 'price', 'theme']
prices:
groups: ['place']
themes:
groups: ['place']
Par défaut, aucune propriété de nos classes n’est affichée pendant la sérialisation. En mettant
l’attribut exclusion_policy à none, nous configurons le sérialiseur pour inclure par défaut
toutes les propriétés de la classe. Nous pourrons bien sûr exclure certaines propriétés à la
demande (exclude: true).
De même, il est aussi possible d’adopter la stratégie inverse à savoir exclure par défaut toutes
les propriétés de nos classes et les ajouter à la demande (expose: true).
Il faut aussi noter que l’attribut attributes dans l’ancien fichier de configuration est remplacé
par properties. Tout le reste est identique à notre ancien fichier de configuration.
La configuration des nouvelles classes devient maintenant :
# src/AppBundle/Resources/config/serializer/Entity.Price.yml
AppBundle\Entity\Price:
exclusion_policy: none
properties:
id:
groups: ['place', 'price']
type:
groups: ['place', 'price']
value:
groups: ['place', 'price']
place:
groups: ['price']
199
III. Amélioration de l’API REST
# src/AppBundle/Resources/config/serializer/Entity.Theme.yml
AppBundle\Entity\Theme:
exclusion_policy: none
properties:
id:
groups: ['place', 'theme']
name:
groups: ['place', 'theme']
value:
groups: ['place', 'theme']
place:
groups: ['theme']
# src/AppBundle/Resources/config/serializer/Entity.User.yml
AppBundle\Entity\User:
exclusion_policy: none
properties:
id:
groups: ['user', 'preference', 'auth-token']
firstname:
groups: ['user', 'preference', 'auth-token']
lastname:
groups: ['user', 'preference', 'auth-token']
email:
groups: ['user', 'preference', 'auth-token']
preferences:
groups: ['user']
# src/AppBundle/Resources/config/serializer/Entity.Preference.yml
AppBundle\Entity\Preference:
exclusion_policy: none
properties:
id:
groups: ['user', 'preference']
name:
groups: ['user', 'preference']
value:
groups: ['user', 'preference']
user:
groups: ['preference']
200
III. Amélioration de l’API REST
# src/AppBundle/Resources/config/serializer/Entity.AuthToken.yml
AppBundle\Entity\AuthToken:
exclusion_policy: none
properties:
id:
groups: ['auth-token']
value:
groups: ['auth-token']
createdAt:
groups: ['auth-token']
user:
groups: ['auth-token']
×
N’oubliez pas de vider le cache pour éviter tout problème.
En testant cette nouvelle configuration, la liste des lieux dans notre application redevient
correcte.
[
{
"id": 1,
"name": "Tour Eiffel",
"address": "5 Avenue Anatole France, 75007 Paris",
"prices": [
{
"id": 1,
"type": "less_than_12",
"value": 5.75
}
],
"themes": [
{
"id": 1,
"name": "architecture",
"value": 7
},
{
"id": 2,
"name": "history",
"value": 6
}
]
},
{
"id": 2,
201
III. Amélioration de l’API REST
"name": "Mont-Saint-Michel",
"address": "50170 Le Mont-Saint-Michel",
"prices": [],
"themes": [
{
"id": 3,
"name": "history",
"value": 3
},
{
"id": 4,
"name": "art",
"value": 7
}
]
}
]
Vous pouvez tester l’ensemble des appels que nous avons déjà mis en place. L’API se comporte
exactement de la même façon.
202
16. La documentation avec OpenAPI
(Swagger RESTFul API)
?
Que serait une API s’il était impossible de comprendre son mode de fonctionnement ?
Parler de documentation dans une API RESTful se rapproche beaucoup d’un oxymore. En effet,
une API dite RESTFul devrait pouvoir être utilisée sans documentation.
Mais si vous vous souvenez bien, notre API n’implémente pas le niveau 3 du modèle de maturité
de Richardson : HATEOAS qui permettrait de l’explorer automatiquement et d’interagir avec
elle. Dès lors, pour faciliter son usage nous devons créer une documentation.
Elle permettra ainsi aux clients de notre API de comprendre son mode de fonctionnement et
d’explorer rapidement les différentes fonctionnalités qu’elle expose.
Il existe un standard appelé OpenAPI, anciennement connu sous le nom de Swagger RESTful
API, permettant d’avoir des spécifications simples pour une documentation exhaustive.
L’objectif de cette partie est d’avoir un aperçu de OpenAPI et de voir comment mettre en place
une documentation en implémentant ces spécifications.
203
III. Amélioration de l’API REST
Bien que le résultat final du fichier OpenAPI soit en JSON, il peut être rédigé aussi bien en
JSON qu’en YAML. Nous préférerons d’ailleurs le YAML par la suite.
Pour créer ce fichier swagger.json, il faut suivre les spécifications qui sont disponibles en
ligne : Spécification OpenAPI (Swagger) .
L’un des moyens les plus simples pour rédiger et tester les spécifications est d’utiliser le site
Swagger Editor . Ce site propose une prévisualisation de la documentation qui sera générée et
des exemples de configuration (en YAML) qui permettent de mieux appréhender les spécifications
d’OpenAPI.
host: rest-api.local
schemes:
- http
produces:
- application/json
- application/xml
consumes:
- application/json
- application/xml
paths: # obligatoire
204
III. Amélioration de l’API REST
Les attributs produces et consumes permettent de décrire les type MIME des réponses
renvoyées et des requêtes acceptées par notre API. Il est possible d’utiliser du Markdown
pour formater les différentes descriptions (attributs description) dans la documentation.
i
Tous les tests se feront en utilisant directement le site http ://editor.swagger.io . Le
fichier swagger.json définitif sera testé en local dans la dernière partie.
host: rest-api.local
schemes:
- http
produces:
- application/json
- application/xml
consumes:
- application/json
- application/xml
paths: # obligatoire
/auth-tokens:
post:
205
III. Amélioration de l’API REST
Voici la base permettant de créer des opérations. Sous l’attribut paths, il faut définir l’URL
de notre ressource et ensuite il faut déclarer les différents verbes HTTP qui sont utilisés sur
celle-ci. Actuellement, nous avons la méthode POST permettant de créer un token. Nous devons
maintenant définir :
— le payload de la requête ;
— la réponse en cas de succès ;
— la réponse en cas d’erreur.
Toutes ces données sont déclarées en utilisant les spécifications de JSON Schema .
# ...
paths: # obligatoire
/auth-tokens:
post:
summary: Authentifie un utilisateur
description: Crée un token permettant à l'utilisateur
d'accéder aux contenus protégés
parameters:
- name: credentials # obligatoire
in: body # obligatoire
required: true
description: Login et mot de passe de l'utilisateur
schema:
type: object
required: [login, password]
properties:
login:
type: string
password:
type: string
responses:
200:
description: Token créé # obligatoire
schema:
type: object
properties:
id:
type: integer
value:
type: string
206
III. Amélioration de l’API REST
created_at:
type: string
format: date-time
user:
type: object
properties:
id:
type: integer
email:
type: string
format: email
firstname:
type: string
lastname:
type: string
400:
description: Donnée invalide # obligatoire
schema:
type: object
required: [message]
properties:
code:
type: integer
message:
type: string
errors:
type: object
properties:
children:
type: object
properties:
login:
type: object
properties:
errors:
type: array
items:
type: string
password:
type: object
properties:
errors:
type: array
items:
type: string
207
III. Amélioration de l’API REST
Il est aussi possible de mieux organiser le fichier en rajoutant une entrée definitions qui
permet de regrouper tous les schémas que nous avons déclarés. Ensuite, il suffira de faire référence
à ces schémas en utilisant l’attribut $ref.
# ...
paths: # obligatoire
/auth-tokens:
post:
summary: Authentifie un utilisateur
description: Crée un token permettant à l'utilisateur
d'accéder aux contenus protégés
parameters:
- name: credentials # obligatoire
in: body # obligatoire
required: true
description: Login et mot de passe de l'utilisateur
schema:
$ref: "#/definitions/Credentials"
208
III. Amélioration de l’API REST
responses:
200:
description: Token créé # obligatoire
schema:
$ref: "#/definitions/AuthToken.auth-token"
400:
description: Donnée invalide # obligatoire
schema:
$ref: "#/definitions/CredentialsTypeError"
definitions:
Credentials:
type: object
required: [login, password]
properties:
login:
type: string
password:
type: string
AuthToken.auth-token:
type: object
required: [id, value, created_at, user]
properties:
id:
type: integer
value:
type: string
title: Token d'authentification
description: Valeur à utiliser dans l'entête X-Auth-Token
created_at:
type: string
format: date-time
user:
type: object
properties:
id:
type: integer
email:
type: string
format: email
firstname:
type: string
lastname:
type: string
CredentialsTypeError:
type: object
required: [message]
209
III. Amélioration de l’API REST
properties:
code:
type: integer
message:
type: string
errors:
type: object
properties:
children:
type: object
properties:
login:
type: object
properties:
errors:
type: array
items:
type: string
password:
type: object
properties:
errors:
type: array
items:
type: string
De la même façon pour documenter la suppression d’un token, nous devons rajouter une nouvelle
URL. Mais cette fois-ci, elle doit être dynamique comme pour les routes Symfony.
# ...
paths: # obligatoire
/auth-tokens:
# ...
/auth-tokens/{id}:
delete:
summary: Déconnecte un utilisateur
description: Supprime le token de l'utilisateur
parameters:
- $ref: "#/parameters/X-Auth-Token"
- name: id # obligatoire
in: path # obligatoire
210
III. Amélioration de l’API REST
responses:
204:
description: Token supprimé # obligatoire
400:
description: Donnée invalide # obligatoire
schema:
$ref: "#/definitions/GenericError"
parameters:
X-Auth-Token:
name: X-Auth-Token # obligatoire
in: header # obligatoire
type: string # obligatoire si le paramètre dans in est
différent de 'body'
required: true
description: Valeur du token d'authentification
definitions:
# ...
GenericError:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
211
III. Amélioration de l’API REST
i
Il existe deux attributs securityDefinitions et security permettant de configurer la
méthode d’authentification sans passer par l’attribut parameters. Mais pour les besoins
de cet exemple, nous ne les utiliserons pas.
Toutes les informations utilisées pour créer ce fichier sont issues des spécifications officielles
d’OpenAPI . Vous pourrez les consulter afin de voir l’ensemble des fonctionnalités qu’offrent
OpenAPI.
212
III. Amélioration de l’API REST
Pour installer Swagger UI, il suffit de le télécharger depuis GitHub . Ensuite, nous allons le
décompresser dans un dossier nommé swagger-ui dans le répertoire web. Nous utiliserons la
version v2.1.4.
i
Si vous utilisez git, il suffit de se placer dans le dossier web et de lancer :
213
III. Amélioration de l’API REST
Depuis l’interface de Swagger Editor, il est possible d’exporter notre documentation au format
JSON. Le fichier swagger.json ainsi obtenu ressemble à :
{
"swagger": "2.0",
"info": {
"title": "Proposition de suggestions API",
"description":
"Proposer des idées de sortie à des utilisateurs en utilisant leurs
"version": "1.0.0"
},
"host": "rest-api.local",
"schemes": [
"http"
],
"produces": [
"application/json",
"application/xml"
],
"consumes": [
"application/json",
"application/xml"
],
"paths": {
// ...
},
"parameters": {
// ...
},
"definitions": {
// ...
}
}
Pour utiliser ce fichier swagger.json, il faut commencer par l’enregistrer dans le dossier
web. Le fichier doit être disponible depuis un navigateur. Ensuite, il faut éditer le fichier
web/swagger-ui/dist/indext.html et éditer les lignes 34 à 39.
214
III. Amélioration de l’API REST
url = "http://petstore.swagger.io/v2/swagger.json";
}*/
var url ="/swagger.json";
En consultant l’URL, nous pouvons maintenant voir notre documentation et même tester les
appels API depuis celui-ci.
Après cette brève initiation à OpenAPI, connu aussi sous le nom de Swagger RESTFul API,
vous avez pu remarquer que l’écosystème autour de cette technologie est assez riche.
Ces spécifications se basent sur un ensemble de standards reconnus comme JSON Schema
qui facilitent grandement sa prise en main.
Le fichier swagger.json ainsi obtenu peut être exploité par beaucoup d’outils qui permettent
d’augmenter notre productivité (Génération de code client, génération de code serveur, interface
de documentation avec bac à sable, etc.).
215
17. Automatiser la documentation avec
NelmioApiDocBundle
Bien que les outils de l’écosystème de OpenAPI (Swagger RESTFull API) soient assez bien fournis,
rédiger manuellement toute la documentation peut se montrer assez rapidement rébarbatif.
En plus, à cause de la séparation entre le code et la documentation, cette dernière risque de ne
pas être mise à jour si le code évolue.
Nous allons donc voir comment automatiser la génération de la documentation dans Symfony
avec le bundle NelmioApiDocBundle.
Cette partie n’abordera pas toutes les fonctionnalités de ce bundle mais permettra d’avoir assez
de bagages pour être autonome.
<?php
# app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
216
III. Amélioration de l’API REST
// ...
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
new AppBundle\AppBundle(),
];
// ...
return $bundles;
}
// ...
}
17.2.1. Configuration
i
Il faut garder en tête que la documentation avec NelmioApiDocBundle est grandement
liée au code.
Sans plus attendre, nous allons l’utiliser pour documenter l’appel qui liste les lieux de notre
application.
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
// ...
/**
* @ApiDoc(
* description="Récupère la liste des lieux de l'application"
* )
*
217
III. Amélioration de l’API REST
*
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
* @QueryParam(name="offset", requirements="\d+", default="", description="
* @QueryParam(name="limit", requirements="\d+", default="", description="N
* @QueryParam(name="sort", requirements="(asc|desc)", nullable=true, descr
*/
public function getPlacesAction(Request $request, ParamFetcher
$paramFetcher)
{
// ...
return $places;
}
// ...
}
Avec juste cette annotation, il est possible de consulter la documentation de notre API. Mais
avant d’y accéder, nous devons avoir une URL dédiée. Et pour ce faire, le bundle propose un
fichier de routage qui permet de configurer cette URL.
# app/config/routing.yml
# ...
nelmio-api-doc:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: /documentation
Nous allons aussi rajouter une règle dans le pare-feu de Symfony afin d’autoriser l’accès à la
documentation sans authentification.
# app/config/secrity.yml
security:
# ...
firewalls:
# disables authentication for assets and the profiler,
adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
doc:
pattern: ^/documentation
security: false
# ...
218
III. Amélioration de l’API REST
Pour avoir une vue complète (comme sur l’image), il faut cliquer sur la méthode GET /places
pour dérouler les détails concernant les filtres. La mise en page de la documentation est
grandement inspiré de Swagger UI.
Le premier point qui devrait vous interpeller est la présence des filtres de FOSRestBundle dans
la documentation. NelmioApiDocBundle a été conçu pour interagir avec la plupart des bundles
utilisés dans le cadre d’une API. Ainsi, les annotations de FOSRestBundle sont utilisées pour
compléter la documentation.
Bien sûr, si nous n’utilisons pas FOSRestBundle, nous pouvons rajouter manuellement des filtres
en utilisant l’attribut filters de l’annotation ApiDoc.
De la même façon, le verbe HTTP utilisé est GET avec une URL /places. Là aussi, les routes
générées par Symfony sont utilisées par NelmioApiDocBundle.
Notre documentation n’est pas encore complète. Le type des réponses renvoyées par notre API
n’est pas encore documenté.
Pour ce faire, il existe un attribut nommé output qui prend comme paramètre le nom d’une
classe ou encore une collection. Cet attribut supporte aussi les groupes de sérialisation que nous
avons déjà définis.
Pour le cas des lieux, nous devons renvoyer une collection de lieux. La documentation s’écrit
donc :
219
III. Amélioration de l’API REST
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
// ...
/**
* @ApiDoc(
* description="Récupère la liste des lieux de l'application",
* output= { "class"=Place::class, "collection"=true, "groups"={"place"}
* )
* @Rest\View(serializerGroups={"place"})
* @Rest\Get("/places")
* @QueryParam(name="offset", requirements="\d+", default="", description="
* @QueryParam(name="limit", requirements="\d+", default="", description="N
* @QueryParam(name="sort", requirements="(asc|desc)", nullable=true, descr
*/
public function getPlacesAction(Request $request, ParamFetcher
$paramFetcher)
{
// ...
}
// ...
}
La documentation devient :
220
III. Amélioration de l’API REST
La documentation est complétée et les attributs ont exactement les bon types définis dans
les annotations Doctrine. Pour obtenir de telles informations, NelmioApiDocBundle utilise le
sérialiseur de JMSSerializerBundle.
×
Par contre, si nous étions restés sur le sérialiseur natif de Symfony qui n’est pas encore
supporté, nous n’aurions pas pu obtenir ces informations.
Les descriptions de tous les attributs sont vides. Pour les renseigner, il suffit de rajouter dans
les entités une description dans le bloc de PHPDoc.
Pour l’entité Place, nous pouvons rajouter :
<?php
/**
* Identifiant unique du lieu
*
* @ORM\Id
* @ORM\Column(type="integer")
221
III. Amélioration de l’API REST
* @ORM\GeneratedValue
*/
protected $id;
De la même façon, pour définir la structure des payloads des requêtes, nous pouvons utiliser un
attribut nommé input qui peut prendre en paramètre, entre autres, une classe qui implémente
l’interface PHP JsonSerializable mais aussi un formulaire Symfony. Et cela tombe bien
puisse que tous nos payloads se basent sur ces formulaires.
Pour tester le bon fonctionnement de cet attribut, nous allons rajouter de la documentation
pour la méthode de création d’un lieu.
<?php
# src/AppBundle/Controller/PlaceController.php
namespace AppBundle\Controller;
// ...
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
// ...
222
III. Amélioration de l’API REST
Pour rajouter des descriptions pour les différents attributs des formulaires, nous pouvons
utiliser une option nommée description rajoutée aux formulaires Symfony par NelmioApi-
DocBundle.
<?php
# src/AppBundle/Form/Type/PlaceType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
223
III. Amélioration de l’API REST
$builder->add('prices', CollectionType::class, [
'entry_type' => PriceType::class,
'allow_add' => true,
'error_bubbling' => false,
'description' => "Liste des prix pratiqués"
]);
}
En définissant l’attribut output, le code de statut associé par défaut est 200. Mais pour la
création d’un lieu, nous devons avoir un code 201. Et de la même façon si le formulaire est
invalide, nous voulons renvoyer une erreur 400 avec les messages de validation. Pour obtenir un
tel résultat, NelmioApiDocBundle met à notre disposition un attribut responseMap.
<?php
# src/AppBundle/Controller/PlaceController.php
224
III. Amélioration de l’API REST
namespace AppBundle\Controller;
// ...
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
// ...
Le paramètre form_errors permet de spécifier le type de retour que nous voulons à savoir les
erreurs de validation.
225
III. Amélioration de l’API REST
Ici, nous avons bien deux réponses selon le code de statut mais pour la réponse lors d’un requête
invalide, le format n’est pas correct (pas d’attribut children, l’attribut status_code s’appelle
code, etc.).
226
III. Amélioration de l’API REST
Pour corriger les petits manquements de NelmioApiDocBundle, nous allons étendre le code de
celui-ci. L’objectif n’est pas d’apprendre le code source de ce bundle mais plutôt de maximiser
son efficacité en l’adaptant à nos besoins.
i
Il est possible d’obtenir de la documentation en redéfinissant manuellement toutes ces
informations manquantes. Mais l’intérêt réel de ce bundle réside dans le fait d’utiliser les
composants déjà existants pour générer la documentation automatiquement. N’hésitez
donc pas à consulter la documentation officielle de NelmioApiDocBundle pour plus
d’informations.
Il n’y a pas de documentation sur comment étendre NelmioApiDocBundle. Mais vu que ce bundle
est open source, il suffit de relire avec attention son code pour comprendre son fonctionnement.
Il en ressort que pour traiter les informations disponibles dans les attributs input et output
de l’annotation ApiDoc, le bundle utilise des parseurs.
Et la documentation officielle nous explique comment en créer et comment l’utiliser.
Nous allons donc créer un parseur capable de générer les erreurs de validation au même format
que FOSRestBundle.
Ce code est grandement inspiré du parseur déjà existant (FormErrorsParser ).
<?php
# src/Component/ApiDoc/Parser/FOSRestFormErrorsParser.php
namespace Component\ApiDoc\Parser;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Parser\ParserInterface;
use Nelmio\ApiDocBundle\Parser\PostParserInterface;
227
III. Amélioration de l’API REST
$params['code'] = [
'dataType' => 'integer',
'actualType' => DataTypes::INTEGER,
'subType' => null,
'required' => false,
'description' => 'The status code',
'readonly' => true
];
$params['message'] = [
'dataType' => 'string',
'actualType' => DataTypes::STRING,
'subType' => null,
'required' => true,
'description' => 'The error message',
'default' => 'Validation failed.',
];
$params['errors'] = [
'dataType' => 'errors',
'actualType' => DataTypes::MODEL,
'subType' => sprintf('%s.FormErrors', $item['class']),
'required' => true,
'description' => 'List of errors',
'readonly' => true,
'children' => [
'children' => [
'dataType' => 'List of form fields',
'actualType' => DataTypes::MODEL,
'subType' => sprintf('%s.Children',
$item['class']),
'required' => true,
228
III. Amélioration de l’API REST
$params['errors']['children']['children']['children'][$name]
= $this->doPostParse($parameter, $name, [$name],
$item['class']);
}
return $params;
}
if ($parameter['actualType'] == DataTypes::COLLECTION) {
$data['children']['children'] = [
'dataType' => 'List of embedded forms fields',
'actualType' => DataTypes::COLLECTION,
'subType' => sprintf('%s.FormErrors',
$parameter['subType']),
'required' => true,
'description' => 'Validation error messages',
'readonly' => true,
229
III. Amélioration de l’API REST
'children' => [
'children' => [
'dataType' => 'Embedded form field',
'actualType' => DataTypes::MODEL,
'subType' => sprintf('%s.Children',
$parameter['subType']),
'required' => true,
'description' => 'List of errors',
'readonly' => true,
'children' => []
]
]
];
$data['children']['children']['children']['children']['chil
= $this->doPostParse($cParameter, $cName,
$cPropertyPath, $parameter['subType']);
}
return $data;
}
}
Ce parseur doit toujours être utilisé avec FormTypeParser qui apporte l’ensemble des informa-
tions issues du formulaire Symfony. Pour l’activer, il faut utiliser l’attribut : fos_rest_form_er
rors (voir la méthode supports).
Pour le déclarer en tant parseur prêt à l’emploi, nous devons créer un service avec le tag
nelmio_api_doc.extractor.parser.
i
Tous les parseurs natifs du bundle sont déclarés avec une priorité de 0. En utilisant une
priorité de 1, nous nous assurons que notre parseur est toujours appelé en dernier.
Pour utiliser notre parseur, nous allons ajuster l’annotation sur le contrôleur des lieux en utilisant
l’attribut fos_rest_form_errors.
230
III. Amélioration de l’API REST
La documentation officielle sur le bac à sable est concise et simple. Les paramètres disponibles
sont d’ailleurs assez proches de ceux d’OpenAPI.
Voyez donc par vous-même.
231
III. Amélioration de l’API REST
Avant de tester ce bac à sable, nous allons rajouter de la documentation pour la création de
token d’authentification. Cela facilitera grandement nos tests.
Vu que toutes nos méthodes nécessites une authentification, il faut d’abord crée un token
d’authentification. Ce token doit être renseigné dans le formulaire api_key.
232
III. Amélioration de l’API REST
Avec la configuration que nous avons mise en place, ce token sera envoyé automatiquement pour
toutes nos requêtes.
Maintenant pour récupérer les lieux de l’application, il suffit de cliquer sur le bouton Try it! .
233
III. Amélioration de l’API REST
!
Avec la version 2.13.0, ce bundle génère un fichier swagger.json en utilisant la version
1.2 des spécifications d’OpenAPI alors qu’il existe une version 2.0. Le fichier généré ne
sera donc pas à jour même si dans la configuration nous mettons 2.0 comme valeur de
l’attribut swagger_version.
En exécutant la commande :
Les fichiers ainsi générés dans le dossier web/swagger peuvent être exploités par tous les outils
compatibles avec OpenAPI.
Pour les tester, il suffit d’éditer le fichier web/swagger-ui/dist/indext.html et de remplacer
la ligne var url ="/swagger.json"; par var url ="/swagger/auth-tokens.json";.
En accédant à l’URL http ://rest-api.local/swagger-ui/dist/index.html , la documentation
générée s’affiche.
ApiDocBundle supporte les différents bundles de Symfony et le tout permet d’avoir un ensemble
harmonieux et facilite les développements.
L’un des problèmes les plus communs lorsque nous écrivons une documentation est de la
maintenir à jour. Avec une documentation proche du code, il est maintenant très facile de la
corriger en même temps que le code évolue.
En effet, un utilisant les annotations de FOSRestBundle, les formulaires de Symfony et les
fichiers de sérialisation de JMSSerializerBundle, nous avons la garantie que la documentation
est toujours à jour avec notre code.
Il ne reste plus qu’à tout mettre en production !
234
18. FAQ
Dans cette section, nous allons aborder quelques points intéressants qui reviennent souvent dans
les questions concernant ce cours.
Les points abordés n’ont pas de relation particulière et peuvent donc être lu dans n’importe
quel ordre.
235
III. Amélioration de l’API REST
i
Comme pour les formats JSON et XML, la génération de réponse au format HTML
est déjà supporté par défaut. Mais en rajoutant la clé templating_formats.html, la
configuration est plus lisible. De plus, nous utilisons templating_formats au lieu de
formats car pour les pages HTML, nous aurons besoin d’un template pour les afficher.
Nous pouvons rajouter autant de règles que nous voulons mais cela peut rapidement montrer
ses limites. Nous avons ainsi la possibilité d’utiliser un autre système plus efficace pour isoler la
partie API et la partie IHM2 de son application.
2. Interface Homme Machine, dans notre cas la page qui s’affiche dans le navigateur.
236
III. Amélioration de l’API REST
Actuellement cela est impossible mais nous pouvons corriger le tir très facilement.
Pour rappel, l’authentification est géré par la classe AuthTokenAuthenticator dont voici un
extrait du code :
Pour obtenir la liste des routes, nous pouvons utiliser la commande php bin/console de
bug:router.
Avec le système de nommage de FOSRestBundle, nous avons des noms simples et surtout qui
décrivent aussi le verbe HTTP associé à la route. Dés lors pour autoriser une action, nous
pouvons nous baser uniquement sur le nom de la route correspondante (le verbe HTTP est
vérifiée indirectement).
Ainsi pour autoriser la création d’utilisateurs et de tokens d’authentification, nous pouvons
simplement utiliser respectivement les routes : post_users et post_auth_tokens.
Le code peut devenir :
Le service HttpUtils étant maintenant inutile, nous pouvons même le retirer de la configuration
des services.
Nous avons pu voir tout au long de ce cours que les contraintes REST permettent de mettre en
237
III. Amélioration de l’API REST
place une API uniforme et facile à prendre en main. La mise en œuvre de ces contraintes offre
un ensemble d’avantages et le framework Symfony dispose d’outils suffisamment matures pour
aider dans les développements.
Ce cours bien qu’étant assez long n’aborde pas tous les concepts de REST ni toutes les
fonctionnalités qu’apportent FOSRestBundle et les différents bundles utilisés. Son objectif est
de présenter de manière succincte l’essentiel des notions à comprendre pour pouvoir développer
une API RESTFul et l’améliorer en toute autonomie.
Le style d’architecture REST ne s’occupe pas des détails d’implémentations mais plutôt du rôle
de chaque composant de notre application.
N’hésitez surtout pas enrichir l’API et à explorer les documentations officielles des différents
outils abordés pour mieux cerner tout ce qu’ils peuvent vous apporter.
238
Liste des abréviations
MIME Multipurpose Internet Mail Extensions. 131, 132, 134, 202
239