Notre API est désormais complète : elle gère le CRUD, la sécurité, la mise en cache et respecte même les 3 niveaux du modèle de Richardson.
Que reste-il ? Eh bien maintenant, cette API, il faut la faire vivre, la faire évoluer. Et pour cela, nous allons finir cette seconde partie avec la gestion du versioning.
Découvrez le versioning
Notre API est basique. Les livres ne possèdent que peu d’informations : un titre, la quatrième de couverture et un auteur.
Imaginons que nous voulions ajouter une information, par exemple un commentaire du bibliothécaire. Cela va faire évoluer notre API et avec la même route, nous allons avoir des informations différentes.
Mais rien ne dit que ceux qui appellent notre API aient prévu ce qu’il faut pour gérer ces nouvelles informations. L’idée est donc de fournir un moyen à ces clients de garder l’ancienne API, alors que la nouvelle est déjà fonctionnelle, pour leur laisser le temps de se mettre à jour.
Mettez en place de nouvelles données
Nous allons rajouter un champ comment
dans la table book
. Rien de neuf ici, nous avons déjà fait des choses similaires dans les chapitres précédents.
Profitons à nouveau de la ligne de commande :
php bin/console make:entity Book
Symfony reconnaît que l’entité existe déjà, et on se contente de rajouter notre nouveau champ.
Pensez à ajouter l’annotation Groups
sur ce champ dans le fichier Book.php
:
<?php
// src\Entity\Book.php
// ...
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(["getBooks"])] // Groupe à ajouter
private $comment;
On met à jour nos fixtures dans AppFixture.php :
<?php
// src\DataFixtures\AppFixtures.php
// ...
for ($i = 0; $i < 20; $i++) {
$book = new Book();
$book->setTitle("Titre " . $i);
$book->setCoverText("Quatrième de couverture numéro : " . $i);
$book->setComment("Commentaire du bibliothécaire " . $i);
$book->setAuthor($listAuthor[array_rand($listAuthor)]);
$manager->persist($book);
}
Ici je me suis contenté de rajouter la ligne avec le setComment
dans la boucle de création du livre.
Et finalement, nous mettons à jour la base de données et nous rejouons nos fixtures :
php bin/console doctrine:schema:update --force php bin/console doctrine:fixtures:load
À ce stade, si tout se passe bien, votre API est déjà mise à jour et vous devriez voir votre commentaire dans Postman :
Mettez en place le versioning
Maintenant, l’idée est de dire que ce champ correspond à une version 2 de notre API. Il se trouve que nous avons installé JMSSerializer, et que JMS gère nativement le versioning, pratique !
Nous allons simplement ajouter dans notre entité une annotation précisant la version à partir de laquelle le champ est présent :
<?php
// src\Entity\Book.php
// ...
use JMS\Serializer\Annotation\Since;
// ...
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(["getBooks"])]
#[Since("2.0")]
private $comment;
#[Since (“2.0”)]
, c’est-à-dire que ce champ n’existe qu’à partir de la version 2.
Et dans le contrôleur, lorsque nous récupérons nos données, nous n’avons plus qu’à ajouter une ligne au contexte pour lui dire quelle est la version de l’API que l’on veut traiter :
<?php
// src\Controller\BookController.php
// ...
#[Route('/api/books/{id}', name: 'detailBook', methods: ['GET'])]
public function getDetailBook(Book $book, SerializerInterface $serializer): JsonResponse
{
$context = SerializationContext::create()->setGroups(["getBooks"]);
$context->setVersion("1.0");
$jsonBook = $serializer->serialize($book, 'json', $context);
return new JsonResponse($jsonBook, Response::HTTP_OK, [], true);
}
Ici, j’ai écrit 1.0
, donc mon nouveau champ ne va pas être visible. En revanche, si je remplace ce numéro par 2.0
, alors on verra mon nouveau champ !
Oui mais… Ici, j’ai écrit directement le numéro de version que je veux, comment faire pour le choisir depuis l’API ?
Spécifiez le numéro de version dans la requête appelante
Il existe plusieurs techniques pour envoyer ce numéro de version, mais l’idée est toujours la même. Il faut que le client (ici, Postman) envoie l’information “Je veux le numéro de version xxx”, et que le contrôleur écoute cette information.
Une des méthodes les plus propres consiste à passer cette information dans le header, et plus précisément dans le champ Accept
.
Ici, j’ai écrit application/json; version=1.0
.
Je précise donc que je veux récupérer du JSON, et que je veux l’API en version 1.0.
Maintenant il nous reste à récupérer cette information.
Nous pourrions lire ce header directement dans le contrôleur, mais cela nécessite un peu de traitement, et sera utile pour plusieurs de nos routes. Du coup, nous allons plutôt créer un service qui va nous permettre de récupérer directement l’information, et qui pourra être appelé par tous les contrôleurs qui en ont besoin.
Cette fois-ci, pas de make, nous devons le créer à la main. Dans le répertoire src
, créez un répertoire Service
et un nouveau fichier VersioningService.php
.
<?php
// src\Service\VersioningService.php
// Création d'un service Symfony pour pouvoir récupérer la version contenue dans le champ "accept" de la requête HTTP.
namespace App\Service;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class VersioningService
{
private $requestStack;
/**
* Constructeur permettant de récupérer la requête courante (pour extraire le champ "accept" du header)
* ainsi que le ParameterBagInterface pour récupérer la version par défaut dans le fichier de configuration
*
* @param RequestStack $requestStack
* @param ParameterBagInterface $params
*/
public function __construct(RequestStack $requestStack, ParameterBagInterface $params)
{
$this->requestStack = $requestStack;
$this->defaultVersion = $params->get('default_api_version');
}
/**
* Récupération de la version qui a été envoyée dans le header "accept" de la requête HTTP
*
* @return string : le numéro de la version. Par défaut, la version retournée est celle définie dans le fichier de configuration services.yaml : "default_api_version"
*/
public function getVersion(): string
{
$version = $this->defaultVersion;
$request = $this->requestStack->getCurrentRequest();
$accept = $request->headers->get('Accept');
// Récupération du numéro de version dans la chaîne de caractères du accept :
// exemple "application/json; test=bidule; version=2.0" => 2.0
$entete = explode(';', $accept);
// On parcours toutes les entêtes pour trouver la version
foreach ($entete as $value) {
if (strpos($value, 'version') !== false) {
$version = explode('=', $value);
$version = $version[1];
break;
}
}
return $version;
}
}
En décortiquant un peu ce code, nous pouvons voir plusieurs éléments.
D’abord le constructeur, celui-ci prend deux paramètres :
la
RequestStack
, qui va nous permettre de récupérer le header de la requête envoyée ;le
parameterBag
, qui va nous permettre de récupérer le numéro de version par défaut de notre API. Ainsi ce numéro pourra être configuré facilement depuis le fichierservices.yaml
, sans modifier le code.
Ensuite, nous avons la méthode getVersion
proprement dite, qui commence par lire l’information Accept
dans le header, la découpe suivant le caractère ;
, et récupère la version.
Si la version n’est pas trouvée, c’est la version par défaut qui est retournée.
À ce stade, il faut encore modifier le fichier services.yaml
pour ajouter le numéro de version par défaut :
parameters:
default_api_version: "2.0"
Et il faut faire appel à notre service depuis le contrôleur :
<?php
// src\Repository\BookRepository.php
// ...
use App\Service\VersioningService;
// ...
#[Route('/api/books/{id}', name: 'detailBook', methods: ['GET'])]
public function getDetailBook(Book $book, SerializerInterface $serializer, VersioningService $versioningService): JsonResponse
{
$version = $versioningService->getVersion();
$context = SerializationContext::create()->setGroups(["getBooks"]);
$context->setVersion($version);
$jsonBook = $serializer->serialize($book, 'json', $context);
return new JsonResponse($jsonBook, Response::HTTP_OK, [], true);
}
Ici, nous avons récupéré directement le service dans les paramètres de la méthode, et nous n’avons plus qu’à nous en servir pour récupérer la version.
Nous voyons ici tout l’intérêt d’utiliser un service, cette méthode getVersion
pourra être réutilisée pour chaque méthode le nécessitant.
Quelques tests pour vérifier, n’hésitez pas à spécifier le numéro de version dans le Accept
, l’enlever, et le passer dans le service.yaml
à 1.0
puis 2.0
pour bien voir toutes les possibilités.
En résumé
Le versioning vous permet de faire évoluer votre API et garder une version stable pour vos clients
Avec JMSSerializer, vous pouvez annoter depuis quand un élément existe, par exemple
#[Since ("3.0")]
, depuis la version 3.0Le client doit passer la version demandée dans le header
Accept
Cette information est récupérée par un service, qui pourra être appelé par les contrôleurs
Félicitations, vous avez atteint la fin de la seconde partie de ce cours ! À ce stade, votre API est complète et peut déjà être utilisée professionnellement.
Dans la troisième et dernière partie, nous verrons quelques outils pour aller plus loin et en particulier, mieux documenter notre API, car la documentation fait également partie des critères qui font la force (ou la faiblesse) d’une API.