• 8 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 27/11/2023

Gérez les erreurs et ajoutez la validation

Gérez les erreurs et ajoutez la validation

Gérez les exceptions

À ce stade, notre API est parfaitement fonctionnelle, mais nous avons cependant laissé de côté un élément pourtant essentiel : la gestion des erreurs.

Dans ce chapitre, nous allons voir d’une part comment centraliser la gestion des erreurs, et d’autre part, comment ajouter facilement de la validation, pour s’assurer que nos données respectent certains critères.

Pour cela, il existe un mécanisme présent dans de nombreux langages, ce sont les exceptions. Lever une exception, c’est clamer au code “Au secours, il y a une erreur !” en espérant qu’un  try catch va attraper cette exception et la traiter.

Dans Symfony en particulier, ce système est évidemment déjà mis en place. Si vous demandez à récupérer un livre avec un id invalide, vous obtenez ce message d’erreur :

On obtient une erreur NotFoundHttpException car l’objet App\Entity\Book n’a pas été trouvé par l’annotation @ParamConverter
Exemple d’erreur : NotFoundHttpException

Comme vous pouvez le voir, il s’agit d’une NotFoundHttpException , et Symfony l’a automatiquement traduite en page HTML, avec les informations nécessaires pour que vous puissiez comprendre le problème.

C’est très pratique lorsqu’on développe un site web, mais ici, nous travaillons avec les API. Nous souhaiterions donc avoir des informations, mais en JSON

L’idée va donc être d’étendre le système d’exception de Symfony, et non pas de laisser Symfony faire par défaut. Nous allons décrire nous-mêmes le comportement attendu.

Comment s’y prendre ?

En créant un ExceptionSubscriber, c'est à dire un élément qui va écouter toutes les exceptions.

Comme souvent, on peut simplement commencer par une petite commande dans le terminal :

php bin/console make:subscriber
On entre la commande dans notre terminal et on répond aux questions : le nom du subscriber et les événements auxquels on va se connecter.
Nous créons un subscriber

Nous répondons aux questions, à savoir :

  • le nom de notre subscriber : ici, j’ai juste repris le nom proposé par défaut, ExceptionSubscriber , cela semble approprié pour un outil qui va nous permettre de gérer des exceptions ; 

  • les événements auxquels nous voulons nous connecter : Symfony nous propose une liste, nous choisissons kernel.exception . 

À ce stade, vous devriez avoir un nouveau fichier, Exceptionsubscriber.php  , avec en particulier une méthodeonKernelException

Cette méthode est un point de passage par lequel toutes les exceptions vont passer. C’est donc l’endroit parfait pour dire en une fois que nous voulons une réponse en JSON et pas en HTML.

<?php 
    // src\EventSubscriber\ExceptionSubscriber.php
    // ...
    
    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();

        if ($exception instanceof HttpException) {
            $data = [
                'status' => $exception->getStatusCode(),
                'message' => $exception->getMessage()
            ];

            $event->setResponse(new JsonResponse($data));
      } else {
            $data = [
                'status' => 500, // Le status n'existe pas car ce n'est pas une exception HTTP, donc on met 500 par défaut.
                'message' => $exception->getMessage()
            ];

            $event->setResponse(new JsonResponse($data));
      }
   }

Si nous décortiquons un peu ce code, en fait, après avoir récupéré l’objet exception  , nous vérifions si cet objet est une HttpException (qui donc correspond à un des codes HTTP, par exemple 404). 

Si c’est le cas, nous récupérons le code et le message, puis nous changeons la réponse de l’événement pour la transformer en JsonResponse.

Au contraire, si ce n’est pas une HttpException, alors, du point de vue du client on va considérer que c’est une erreur “générique”. Le code associé à ce type d’erreur est 500. Donc on renseigne ce code ainsi que le message d’erreur, et on retourne à nouveau une JsonResponse. 

Voyons maintenant la même requête avec Postman, c’est-à-dire le chargement d’un livre avec un id qui n’existe pas :

On essaie de charger un id invalide avec Postman, qui nous renvoie un 404 Not found ainsi qu’un message en JSON “App\Entity\Book object not found by the @ParamConverter annotation”.
Test avec Postman : chargement d’un livre avec un id invalide

Et voilà, une belle réponse en JSON. :-)

Notez qu’ici le traitement que j’ai fait dans la méthode onKernelException est simple, je me suis contenté de récupérer le code d’erreur et le message. Libre à vous de donner plus ou moins d’informations en fonction des besoins spécifiques de votre application !

Validez des données

Bravo, vous avez maintenant un système de gestion d’exception simple et centralisé. Mais allons un peu plus loin !

Essayons par exemple de créer un livre, mais en oubliant complètement son titre :

Quand on essaie de créer un livre sans titre, Postman nous envoie une erreur 500 et un message pour dire que le champ ‘title’ ne peut pas être vide.
Test avec Postman : tentative d’enregistrement avec un body invalide

Comme nous avons dit à la base de données que le titre ne pouvait pas être vide, c’est elle qui a refusé l’insertion.

Cependant, du point de vue de l’utilisateur, ce message est très technique. Je dirais même qu’il ressemble presque à un bug. Ce que nous aimerions ici, ce n’est pas une erreur 500, mais une vraie erreur HTTP qui nous dirait  400 - Bad Request . Cela indiquerait clairement à l’utilisateur que le problème ne vient pas du serveur, mais bien du fait qu’il a mal forgé sa requête

En fait, il faudrait s’assurer que le livre que l’utilisateur veut créer est bien valide.

Vous vous en doutez, Symfony, là encore, a tout prévu.

Première étape, installez le package qui nous permet d’utiliser les validateurs conçus pour nous :

composer require symfony/validator doctrine/annotations

Ensuite, directement au niveau des entités, il va être possible d’écrire desassert. Ils vont décrire les données que nous attendons, et même le message d’erreur en cas d’échec. 

Voici ce que nous pourrions écrire pour le titre, par exemple :

<?php 
    // src\Entity\Book.php
    // ...
    #[ORM\Column(type: 'string', length: 255)]
    #[Groups(["getBooks", "getAuthors"])]
    #[Assert\NotBlank(message: "Le titre du livre est obligatoire")]
    #[Assert\Length(min: 1, max: 255, minMessage: "Le titre doit faire au moins {{ limit }} caractères", maxMessage: "Le titre ne peut pas faire plus de {{ limit }} caractères")]
    private $title;

Ici, nous avons indiqué deux éléments :

  • Assert\NotBlank : pour s’assurer que le titre ne peut pas être null, et indiquer le message à retourner en cas de problème ;

  • Assert\Length : avec les tailles minimales et maximales, et les messages associés. 

Il ne reste plus maintenant qu’à vérifier que nos entités sont valides juste avant la création, directement dans le contrôleur :

<?php 
    // src\Controller\BookController.php
    // ...
    
#[Route('/api/books', name:"createBook", methods: ['POST'])]
    public function createBook(Request $request, SerializerInterface $serializer, EntityManagerInterface $em, UrlGeneratorInterface $urlGenerator, AuthorRepository $authorRepository, ValidatorInterface $validator): JsonResponse 
    {
        $book = $serializer->deserialize($request->getContent(), Book::class, 'json');
        
        // On vérifie les erreurs
        $errors = $validator->validate($book);

        if ($errors->count() > 0) {
            return new JsonResponse($serializer->serialize($errors, 'json'), JsonResponse::HTTP_BAD_REQUEST, [], true);
        }
    
        $em->persist($book);
        $em->flush();

        $content = $request->toArray();
        $idAuthor = $content['idAuthor'] ?? -1;
    
        $book->setAuthor($authorRepository->find($idAuthor));
        $jsonBook = $serializer->serialize($book, 'json', ['groups' => 'getBooks']);
        $location = $urlGenerator->generate('detailBook', ['id' => $book->getId()], UrlGeneratorInterface::ABSOLUTE_URL);

        return new JsonResponse($jsonBook, Response::HTTP_CREATED, ["Location" => $location], true);

    }

Notez l’apparition dans les paramètres de la méthode d’un nouvel élément : leValidatorInterface

Ce validator va tout simplement valider notre entité et retourner un objet  error contenant l’ensemble des erreurs. 

Si cet objet error n’est pas vide, c’est que la demande initiale n’était pas correcte. On retourne donc une JsonResponse avec notre erreur  400 - Bad Request .

Voyons maintenant le résultat :

Cette fois-ci, on obtient bien un statut 400 Bad Request avec le message d’erreur “title: Le titre du livre est obligatoire”.
Test avec Postman : chargement d’un livre avec un id invalide et une réponse explicitant l’erreur

Nous avons maintenant un statut  400 - Bad Request , bien plus approprié, et même le message d’erreur.

Là encore, j’insiste, nous n’avons fait aucune mise en forme pour le message d’erreur, mais libre à vous de ne retourner que les informations que vous estimerez pertinentes.

Exercez-vous

C’est désormais à votre tour de rajouter une ou plusieurs validations. En particulier, le nom d’un auteur ne peut pas être vide !

N’oubliez pas non plus, la création n’est pas le seul endroit où l’on peut spécifier des champs !

En résumé

  • Les exceptions sont un mécanisme présent directement au niveau de PHP pour gérer les erreurs.

  • L'  ExceptionSubscriber est un mécanisme, au niveau de Symfony, qui permet au framework de centraliser cette gestion d'erreur.

  • Les  assert  permettent de spécifier, directement au niveau des entités, quel est le format attendu pour chaque donnée grâce aux annotations.

  • Ensuite dans le contrôleur, il n'y a plus qu'à utiliser un Validator pour s'assurer que les données d'une entité sont bien conformes à ce qui est attendu.

Rendez-vous dans le prochain chapitre pour gérer la sécurité grâce à JWT.

Exemple de certificat de réussite
Exemple de certificat de réussite