• 50 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 29/07/2019

Tutoriel - Gestion d'erreurs via un listener sur l'évènement kernel.exception

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Dans ce tutoriel, nous souhaitons que lorsque l'exception de type Symfony\Component\HttpKernel\Exception\NotFoundHttpException est lancée, une réponse avec un corps de ce type soit envoyé :

{
  "code": 404,
  "messages": "Message d'erreur."
}

Le but est de s'assurer qu'il est aussi possible d'ajouter du code dédié en fonction du type d'exception. Nous allons donc faire en sorte que lorsqu'un contributeur au projet souhaitera ajouter un nouveau moyen de gérer d'autres types d'exceptions, le développeur n'ait pas à modifier le code déjà en place, afin de respecter le principe "Ouvert/Fermé" provenant des principes SOLID.

Avant de se lancer, voici les étapes que nous allons dérouler ensemble :

  1. dans un premier temps, nous allons créer une classe en charge de mettre en forme les informations qu'il faudra ensuite sérialiser - le but est de ne garder que les informations importantes pour le client. Nous la déclarerons en tant que service et nous la taguerons avecapp.normalizer;

  2. puis nous créerons un listener sur l'évènementkernel.exceptionen charge d'exécuter le bon service (tagué avecapp.normalizer) en fonction du type de l'exception ;

    • dans un premier temps, il faut déterminer le type d'exception lancée ;

    • puis trouver parmi les services celui qui supporte le type d'exception trouvé ;

    • appeler la méthode normalize sur le service trouvé, qui doit retourner un objet normalisé ;

    • créer la réponse et appeler la méthodesetResponsesur l'évènement afin que celle-ci soit renvoyée au client.

  3. Enfin, il s'agit de faire en sorte que les services tagués avecapp.normalizersoient accessibles depuis une propriété du listener. Nous allons donc créer un compiler pass en charge de récupérer tous les services tagués avecapp.normalizeret de les charger dans le listener.

  4. Et voilà, nous aurons terminé. :-°

Étape 1 : créer le premier service en charge de la normalisation des informations pour le type d'exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException

Je vous invite donc à créer le service en charge de normaliser les données que nous allons fournir à nos utilisateurs d'API. Dans notreAppBundle, il faut créer un dossierNormalizer. À l'intérieur, nous allons créer trois éléments :

  • a. une interface pour assurer un contrat sur l'implémentation de la méthode normalize et  supports,

  • b. une classe abstraite avec l'implémentation de la méthodesupports,

  • c. le premier service qui est en charge de la normalisation des informations de l'exception de type  Symfony\Component\HttpKernel\Exception\NotFoundHttpException.

a/ InterfaceNormalizerInterfacedans le dossier src/AppBundle/Normalizer :

<?php

namespace AppBundle\Normalizer;

interface NormalizerInterface
{
    public function normalize(\Exception $exception);

    public function supports(\Exception $exception);
}

b/ Classe abstraiteAbstractNormalizer dans le dossier src/AppBundle/Normalizer :

<?php

namespace AppBundle\Normalizer;

abstract class AbstractNormalizer implements NormalizerInterface
{
    protected $exceptionTypes;

    public function __construct(array $exceptionTypes)
    {
        $this->exceptionTypes = $exceptionTypes;
    }

    public function supports(\Exception $exception)
    {
        return in_array(get_class($exception), $this->exceptionTypes);
    }
}

c/ Service en charge de la normalisation d'une exception du typeSymfony\Component\HttpKernel\Exception\NotFoundHttpException

<?php

namespace AppBundle\Normalizer;

use Symfony\Component\HttpFoundation\Response;

class NotFoundHttpExceptionNormalizer extends AbstractNormalizer
{
    public function normalize(\Exception $exception)
    {
        $result['code'] = Response::HTTP_NOT_FOUND;

        $result['body'] = [
            'code' => Response::HTTP_NOT_FOUND,
            'message' => $exception->getMessage()
        ];

        return $result;
    }
}

Nous prenons soin d'étendre la classe abstraiteAbstractNormalizerafin de bénéficier de l'implémentation de la méthode supports.

Souvenez-vous, nous avions convenu que dans le contenu de la réponse il devait y avoir les informations suivantes : le code HTTP (code status) et le message d'erreur.

Nous construisons donc un tableau avec ces informations ! ^^

Il nous faut le déclarer en tant que service avec le tagapp.normalizer:

services:
    app.normalizer.resource_validation_exception:
        class: AppBundle\Normalizer\NotFoundHttpExceptionNormalizer
        arguments:
            - { type: 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' }
        tags:
            - { name: app.normalizer }

Il ne faut pas oublier d'indiquer le (ou les) type(s) pris en charge par le normalizer : nous injectons donc cette information via le constructeur.

Et enfin, nous ajoutons le tagapp.normalizerau service, ce qui nous sera utile pour l'étape 3 de notre développement.

Étape 2 : créer le listener sur l'évènement kernel.exception en charge d'exécuter le bon normalizer (service)

Il nous faut créer le dossier EventSubscriber dans le dossiersrc/AppBundle:

<?php

namespace AppBundle\EventSubscriber;

use AppBundle\Normalizer\NormalizerInterface;
use JMS\Serializer\Serializer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class ExceptionListener implements EventSubscriberInterface
{
    private $serializer;
    private $normalizers;

    public function __construct(Serializer $serializer)
    {
        $this->serializer = $serializer;
    }

    public function processException(GetResponseForExceptionEvent $event)
    {
        $result = null;

        foreach ($this->normalizers as $normalizer) {
            if ($normalizer->supports($exception)) {
                $result = $normalizer->normalize($event->getException());
                
                break;
            }
        }
        
        if (null == $result) {
            $result['code'] = Response::HTTP_BAD_REQUEST;

            $result['body'] = [
                'code' => Response::HTTP_BAD_REQUEST,
                'message' => $event->getException()->getMessage()
            ];
        }

        $body = $this->serializer->serialize($result['body'], 'json');

        $event->setResponse(new Response($body, $result['code']));
    }

    public function addNormalizer(NormalizerInterface $normalizer)
    {
        $this->normalizers[] = $normalizer;
    }
    
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::EXCEPTION => [['processException', 255]]
        ];
    }
}

Nous devons donc le déclarer en tant que service et lui injecter le serializer de la librairie JMSSerializer afin de pouvoir sérialiser les informations :

services:
    app.exception_subscriber:
        class: AppBundle\EventSubscriber\ExceptionListener
        arguments:
            - '@jms_serializer'
        tags:
            - { name: kernel.event_subscriber }

Dans la méthodeprocessException, il s'agit de trouver parmi tous les normalizers (accessibles depuis la propriété$normalizers ), le service qui sera en charge de retourner l'objet normalisé comme il faut. Nous devons ensuite sérialiser l'objet normalisé et renvoyer la réponse avec ce qui a été sérialisé.

N'oublions pas d'implémenter une méthode en charge d'ajouter les normalizers à notre listener. Cette méthode sera appelée dans le compiler pass que nous allons implémenter dans la prochaine étape.

Étape 3 : implémentation du compiler pass en charge d'ajouter tous les normalizers au listener

Il nous faut créer le dossier DependencyInjection, danssrc/AppBundle:

<?php

namespace AppBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class ExceptionNormalizerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $exceptionListenerDefinition = $container->findDefinition('app.exception_subscriber');
        $normalizers = $container->findTaggedServiceIds('app.normalizer');

        foreach ($normalizers as $id => $tags) {
            $exceptionListenerDefinition->addMethodCall('addNormalizer', [new Reference($id)]);
        }
    }
}

Il suffit ensuite de le déclarer dans la classeAppBundle:

<?php

namespace AppBundle;

use AppBundle\DependencyInjection\Compiler\ExceptionNormalizerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class AppBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new ExceptionNormalizerPass());
    }
}

 Et voilà ! Le tour est joué. :-°

Pour tester ce que nous venons d'implémenter, il suffit d'effectuer une requête sur une URL qui n'existe pas afin d'obtenir une 404 :

Réponse générée grâce au listener et normalizer
Réponse générée grâce au listener et normalizer

 

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