• 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

Rendez votre API autodécouvrable

Pour avoir une API totalement REST, il faut respecter tous les niveaux du modèle de maturité de Richardson et, en particulier, il faut que notre API soit autodécouvrable

En d’autres termes, lorsque nous accédons à une ressource, nous devons avoir directement les URL pour explorer cette ressource

Un exemple typique : lorsque nous récupérons la liste des livres à disposition, nous devons, pour respecter cette contrainte, également récupérer les URL de chacun de ces livres, ainsi que, le cas échéant, les URL pour les mettre à jour et les modifier.

C’est ce qu’on appelle “The Glory of REST” (en français, la gloire du REST).

Les 4 niveaux : 0, Marécage du Plain Old XML ; 1, Ressources ; 2, Méthodes HTTP ; 3, Contrôles hypermédia.
Niveaux de respect du modèle de maturité de Richardson

Installez HATEOAS et JMSSerializer

Nous pourrions mettre ce dernier niveau en œuvre à la main, en récupérant nous-mêmes les routes que nous voulons indiquer, et en les sérialisant en même temps que les données.

Cependant, c’est assez fastidieux à faire, et nous risquerions de ne pas respecter les standards… De plus, les projets nécessitant le niveau 3 de Richardson sont souvent des projets relativement complexes, et il est bien plus pratique de déléguer cette tâche à un outil externe. 

Cet outil va être dans notre cas : HATEOAS.

Cet outil a cependant besoin d’un serializer un peu plus abouti que celui de Symfony pour fonctionner, le JMSSerializer.

Heureusement, tout est fourni dans un seul et même bundle :

composer require willdurand/hateoas-bundle

Composer va vous demander si vous voulez exécuter des recettes, celles-ci permettent de créer notamment les fichiers de configuration nécessaires à ces librairies, dites oui. :)

Remplacez le sérialiser de Symfony par JMSSerializer

Maintenant que le bundle est installé, et avant de regarder le fonctionnement de HATEOAS, il va falloir utiliser le nouveau serializer.

Un nouveau fichier, jms_serializer.yaml , devrait avoir été créé. Nous allons commencer par le mettre à jour :

# config\packages\jms_serializer.yaml

jms_serializer:
    visitors:
        xml_serialization:
            format_output: '%kernel.debug%'
    property_naming:
        id: jms_serializer.identical_property_naming_strategy

Le début devrait normalement être présent, mais nous allons ajouter la propriété property_naming .

Celle-ci nous permet de continuer à utiliser le camel case dans nos appels ( coverText et pas cover_text  ), comme nous l’a proposé Symfony par défaut jusqu’à présent.

Mettez à jour les contrôleurs et les entités

Commençons par le fichier BookController.php  .

Il va falloir remplacer le use  concernant le serializer de Symfony par un use  qui concerne JMSSerialiser.

<?php 
    // src\Controller\BookController.php
    // ...

// Supprimez cette ligne :
use Symfony\Component\Serializer\SerializerInterface;

// Ajoutez celle ci :
use JMS\Serializer\Serializer;

Mon éditeur de texte me souligne en rouge certains éléments :

[‘groups’ => ‘getBooks’] est souligné en rouge.
Qu’est-ce qui se passe ?

C’est parce que même si on utilise toujours un $serializer  qui vient de SerializerInterface , il ne s’agit plus du même serializer

On est passé à JMSSerializer, et donc son fonctionnement diffère légèrement. Notamment en ce qui concerne le contexte de sérialisation, c'est-à-dire les informations qui permettent de savoir comment sérialiser (ici, la gestion des groupes, par exemple). 

JMSSerializer étant construit de la même manière, nous aurons peu de choses à changer, mais parmi elles, il faut mettre à jour la syntaxe pour utiliser les “groups”.

Et voici le nouveau code

<?php
    // src\Controller\BookController.php
    // ...

use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;

    // ...

    #[Route('/api/books/{id}', name: 'detailBook', methods: ['GET'])]
    public function getDetailBook(Book $book, SerializerInterface $serializer): JsonResponse 
    {
        $context = SerializationContext::create()->setGroups(['getBooks']);
        $jsonBook = $serializer->serialize($book, 'json', $context);
        return new JsonResponse($jsonBook, Response::HTTP_OK, [], true);
    }

Pour faire simple, JMS crée un objet SerializationContext  et lui passe le groupe : c'est le même principe qu'avant, mais avec une écriture différente.

À ce stade, si on teste on obtient…

Trois éléments sont présents dans le Body de Postman, mais il n’y a rien entre les crochets.
Test avec Postman : mise en place du JMSSerializer

Nous avons bien nos trois éléments comme demandé, mais ils sont vides. Cela ressemble très fort à ce qui se passerait si on indiquait un mauvais nom de groupe.

C’est presque le cas ! En fait, vu que nous sommes passés sur JMSSerializer, nous devons maintenant également modifier les use qui sont dans les entités.

<?php 
    // src\Entity\Book.php et src\Entity\Author.php
    // ...
    
// Annotation à supprimer
use Symfony\Component\Serializer\Annotation\Groups;

// Annotation à rajouter :
use JMS\Serializer\Annotation\Groups;

On relance, et maintenant nous avons le bon résultat :

Cette fois-ci, dans le Body de Postman nous avons les 3 éléments avec leurs données
Test avec Postman : nous obtenons bien nos 3 éléments

Gérez le cas du PUT

Souvenez-vous, dans le cas de l’update, nous récupérions l’entité visée, et nous nous étions arrangés pour désérialiser directement à l’intérieur.

Par exemple, quand nous éditions le livre avec l’id 42, Symfony nous fournissait l’entité correspondant à l’id 42 et désérialisait en utilisant cette entité pour base.

C’est faisable également avec jms_serializer , mais c’est moins pratique à mettre en place, comme vous pouvez le voir dans la documentation de JMSSerializer (en anglais). Je vais donc en profiter pour vous montrer une autre technique, plus simple mais très classique. Elle consiste tout simplement à  :

  • récupérer l’entité correspondant à l’id (grâce au ParamConverter, nous l’obtenons directement dans $currentBook) ;  

  • désérialiser cette entité dans une nouvelle entité vierge (dans $newbook) ;

  • recopier les éléments de l’une dans l’autre ($currentBook->set…).

<?php 
    // src\Controller\BookController.php
    // ...
    
    #[Route('/api/books/{id}', name:"updateBook", methods:['PUT'])]
    #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour éditer un livre')]
    public function updateBook(Request $request, SerializerInterface $serializer, Book $currentBook, EntityManagerInterface $em, AuthorRepository $authorRepository, ValidatorInterface $validator, TagAwareCacheInterface $cache): JsonResponse 
    {
        $newBook = $serializer->deserialize($request->getContent(), Book::class, 'json');
        $currentBook->setTitle($newBook->getTitle());
        $currentBook->setCoverText($newBook->getCoverText());

        // On vérifie les erreurs
        $errors = $validator->validate($currentBook);
        if ($errors->count() > 0) {
            return new JsonResponse($serializer->serialize($errors, 'json'), JsonResponse::HTTP_BAD_REQUEST, [], true);
        }

        $content = $request->toArray();
        $idAuthor = $content['idAuthor'] ?? -1;
    
        $currentBook->setAuthor($authorRepository->find($idAuthor));

        $em->persist($currentBook);
        $em->flush();

        // On vide le cache.
        $cache->invalidateTags(["booksCache"]);

        return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT);
    }

Et voilà, désormais JMS_Serializer  est en place.

Mettez en place HATEOAS

Jusqu’ici, nous avons certes remplacé le serializer de Symfony par JMSSerializer, mais sur notre projet, cela ne change encore rien. Nous l’avons fait uniquement parce que HATEOAS réclame ce nouveau serializer pour fonctionner.

Le niveau 3 du modèle de Richardson explique que lorsqu’on récupère une entité, on doit en même temps récupérer des informations sur les autres routes possibles pour cette entité. 

Voyons comment mettre cela en place avec HATEOAS, sur notre entité Book :

<?php
    // src\Entity\Book.php
    // ...

use Hateoas\Configuration\Annotation as Hateoas;

    // ...
 

/**
 * @Hateoas\Relation(
 *      "self",
 *      href = @Hateoas\Route(
 *          "detailBook",
 *          parameters = { "id" = "expr(object.getId())" }
 *      ),
 *      exclusion = @Hateoas\Exclusion(groups="getBooks")
 * )
 *
 */
#[ORM\Entity(repositoryClass: BookRepository::class)]
class Book
{

Les annotations de HATEOAS définissent une relation, c’est-à-dire un nouveau lien qui va apparaître. Le premier lien est self , il s’agit du lien vers la ressource que l’on est en train de regarder. 

Le paramètre href va contenir la route concernée. Ici, nous avons detailBook , car c’est la route pour avoir le détail d’un livre. Nous passons le paramètre id  à cette route. 

Nous voyons également ici un paramètre : exclusion . Celui-ci permet de déterminer dans quel cas les informations vont apparaître ou pas. Ici, nous précisons simplement le groupe.  

Faisons pareil pour le delete  et le update , et ajoutons ces éléments sous le premier:

<?php
    // src\Entity\Book.php
    // ...

/*
 * @Hateoas\Relation(
 *      "delete",
 *      href = @Hateoas\Route(
 *          "deleteBook",
 *          parameters = { "id" = "expr(object.getId())" },
 *      ),
 *      exclusion = @Hateoas\Exclusion(groups="getBooks", excludeIf = "expr(not is_granted('ROLE_ADMIN'))"),
 * )
 *
 * @Hateoas\Relation(
 *      "update",
 *      href = @Hateoas\Route(
 *          "updateBook",
 *          parameters = { "id" = "expr(object.getId())" },
 *      ),
 *      exclusion = @Hateoas\Exclusion(groups="getBooks", excludeIf = "expr(not is_granted('ROLE_ADMIN'))"),
 * )
 *
 */
#[ORM\Entity(repositoryClass: BookRepository::class)]
#[ApiResource()]
class Book

(...)

Notez le paramètre “exclusion” qui évolue, nous précisons maintenant que le lien sera exclu de la liste si l’utilisateur ne possède pas le ROLE_ADMIN . En effet, seul l’administrateur peut effectuer un delete  ou un update .

Voyons maintenant le résultat :

Dans l’élément _links, il y a des liens self, delete et update.
Le livre a maintenant des liens _links

Vous pouvez constater l’apparition d’un nouvel élément, _links, qui contient directement les liens vers les ressources self ,  delete  et update . 

Voyons également ce qui se passe si on récupère tous les livres :

livre contient l’élément _links avec les 3 liens self, delete et update.
En effet, tous les livres ont maintenant des liens _links

Les liens sont bien mis à jour et en plus, le tout respecte le standard HAL qui est un des plus utilisés pour mettre en œuvre le niveau 3 du modèle de Richardson.

Allez plus loin

Nous allons nous arrêter ici, mais sachez qu’il est possible de configurer plus finement ces éléments.

Il est également possible de spécifier des ressources associées (embed), ce qui dans certains cas peut s’avérer très pratique, notamment si vous avez besoin d’intégrer une ressource externe dans votre API.

Je vais vous laisser explorer les possibilités offertes par HATEOAS grâce à la documentation officielle (en anglais). 

Exercez-vous

Nous avons mis à jour notre projet pour utiliser JMS et HATEOAS pour les livres, mais il reste les auteurs à gérer de la même manière ! :)

N’hésitez pas à essayer d’autres options grâce à la documentation officielle pour avoir une idée des possibilités offertes par cette librairie. 

En résumé

  • HATEOAS est une bibliothèque qui permet de gérer facilement l’autodécouvrabilité. 

  • JMSSerializer est un serializer, au même titre que le serializer de Symfony, mais qui possède certaines fonctionnalités supplémentaires nécessaires à HATEOAS.

  • Pour le versioning, on essaie généralement d’éviter de passer les informations sur la version attendue dans l’URL, et on préfère utiliser le champAccept . 

  • Un service permet de déporter un traitement à l’extérieur d’un contrôleur, ce qui est pratique notamment si ce traitement peut être demandé par plusieurs contrôleurs. 

 Bravo à vous ! Il vous reste un dernier chapitre dans cette deuxième partie du cours. Découvrons comment versionner une API - suivez-moi !

Example of certificate of achievement
Example of certificate of achievement