16/06/2019

Redirection depuis un service Symfony

Récemment, un collègue s'est retrouvé confronté à une problématique intéressante : comment effectuer une redirection depuis un service Symfony ? La première solution est de gérer les redirections depuis les contrôleurs utilisant ce service. Toutefois, cette approche pose un problème : comment être certain de ne pas oublier d'effectuer une redirection et comment s'assurer que les futures utilisations de ce service réalise cette redirection ? Il est possible de répondre à ces deux problématiques en s'appuyant sur le système d'événements proposé par Symfony.

Système d'événements

Symfony propose, à travers le composant HttpKernel, un grand nombre d'événements permettant aux développeurs de :

Diagramme d'événements

Le diagramme ci-dessus est une représentation simplifiée de la transformation d'une requête en une réponse. Comme on peut le constater, la réponse est normalement retournée par un contrôleur, mais le déclenchement d'une exception permet de renvoyer une autre réponse au navigateur. C'est cette deuxième possibilité que l'on va exploiter pour résoudre notre problème.

Implémentation de l'exception

Notre objectif est d'effectuer une redirection vers une page spécifique, il est donc nécessaire d'avoir au minimum une url et un code http (301 ou 302). De plus, afin de pouvoir lever l'exception, il est nécessaire d'étendre la classe \Exception.

<?php

namespace App\Exception;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;

/**
 * RedirectException is thrown when the user have to be redirect to a url outside of a controller.
 *
 * @author Grégory LEFER <contact@glefer.fr>
 */
class RedirectException extends \Exception
{
    /**
     * @var string target url where to redirect the user
     */
    private $url;
    /**
     * @var int Http code of the redirection (301, 302,..)
     */
    private $codeHttp;

    /**
     * RedirectException constructor ...
     */
    public function __construct(
        string $url, int $codeHttp = Response::HTTP_MOVED_PERMANENTLY,
        string $message = "", int $code = 0, \Throwable $previous = NULL)
    {
        parent::__construct($message, $code, $previous);
        $this->url = $url;
        $this->codeHttp = $codeHttp;
    }

    /**
     * Returns a RedirectResponse to the given URL ...
     */
    public function getResponse(): Response
    {
        return new RedirectResponse($this->url, $this->codeHttp);
    }
}

Grâce à cette classe, il sera possible d'effectuer une redirection depuis n'importe où en déclenchant cette exception.

    throw new  \App\Exception\RedirectException($router->generate('my_target_route'));

Interception de l'exception

Il ne reste plus qu'à intercepter cette exception afin d'effectuer la rediriction vers la page demandée. Pour cela, nous allons implémenter un Listener écoutant l'événement kernel.exception.

<?php

namespace App\EventListener;

use App\Exception\RedirectException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

/**
 * Listen the kernel.exception event in order to redirect to an url
 *
 * @author Grégory LEFER <contact@glefer.fr>
 */
class RedirectExceptionListener
{

    /**
     * Return a RedirectResponse if a RedirectException is thrown
     */
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        if (($exception = $event->getException()) instanceof RedirectException) {
            $event->setResponse($exception->getResponse());
        }
    }

}

Pour que ce listener soit pris en compte, il est nécessaire de l'enregistrer en tant que service. Pour cela, ajoutez les lignes suivantes dans le fichier config/services.yaml.

services:
    ...
    App\EventListener\RedirectExceptionListener:
        tags:
            - { event: kernel.exception, name: kernel.event_listener }

Vous pouvez à présent utiliser cette exception, depuis un service par exemple, afin de rediriger vos utilisateurs sur n'importe quelle page.

Bon week-end à tous.