• 8 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 11/27/23

Versionnez votre API

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 :

Dans le Body de Postman, on voit bien l’élément “comment” dans un des livres.
Test avec Postman : le commentaire est bien présent

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 .

On spécifie dans Accept ce que l’on veut récupérer (du JSON) et la version (1.0).
Nous ajoutons un champ Accept au Headers du Postman

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 fichier services.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.0

  • Le 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. 

Example of certificate of achievement
Example of certificate of achievement