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 :
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
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 :
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 :
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 :
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.