KeiruaProd

Redirection après login sous Symfony2

Vous connaissez sans doute FOSUserBundle, un des bundle les plus connus et utilisés de Symfony2. Il permet de gérer l’enregistrement et l’authentification de vos utilisateurs de manière très simple, ce qui permet de se concentrer sur le code métier des applications.

La documentation du bundle est très complète et bien fournie, mais ne couvre pas un détail : lorsqu’un utilisateur se connecte dans votre application (avec son login/mot de passe par exemple), si l’authentification réussit, alors l’utilisateur est redirigé vers la page d’accueil.

Mais cela n’est pas forcément ce que l’on souhaite ! On peut vouloir qu’il soit redirigé ailleurs, par exemple vers sa page de profil. Et quand c’est comme ça, on fait comment ? C’est ce que je vais vous expliquer.

Si ce problème n’est pas indiqué dans la documentation du bundle, c’est car l’authentification est en fait gérée par symfony, et non par le bundle. Cela n’en reste pas moins un problème. Dustin Dobervich a proposé une première solution, il y a un bon moment. Cette solution a depuis été améliorée, mais je vais vous présenter les 2, car elles permettent de mieux comprendre comment fonctionne le framework en interne.

Si vous avez lu ma traduction des articles de Fabien Potencier sur les composants du framework, vous savez que moteur interne interne de Symfony2 utilise beaucoup les évènements. Il est possible « d’écouter » ces évènements, afin de réagir lorsqu’ils arrivent. Pour cela, on crée ce qu’il s’appelle un listener.

En ce qui nous concerne, la solution initiale proposée par Dustin consiste à écouter 2 évènements : security.interactive_login et kernel.response. Le premier évènement est créé après que l’authentification ai réussie. Le second est généré lorsqu’une réponse, n’importe laquelle, est créée.

Intuitivement, il suffit donc de réaliser une redirection lors du second évènement. Mais si nous faisons cela sans conditions, n’importe quelle page va nous rediriger vers la page de profil, car notre évènement va se déclencher à chaque fois.
Pour palier à celà, notre pouvons définir un flag qui indiquera, grâce à l’évènement security.interactive_login, lorsqu’il faudra ou non effectuer une redirection.

Cette solution fonctionne, et je vous invite à regarder le code proposé dans l’article original pour voir son implémentation (la problématique est légèrement différente). L’inconvénient, c’est qu’on écoute l’évènement kernel.response en permanence, ce qui ne sert à rien : exécuter du code inutilement n’est jamais une bonne chose. En effet, la condition « cette réponse fait-elle suite à une connection réussie ? » échoue la plupart du temps, car les utilisateurs ne passent pas leur temps à se connecter. Bref niveau performances c’est pas terrible (même si ça n’est pas forcément critique), et niveau conception c’est pas fou non plus.

Pour pallier à celà, nous n’allons écouter en permanence que l’évènement security.interactive_login, qui n’est pas déclenché très souvent, et uniquement quand nous en avons besoin. Lorsque cela arrive, nous allons demander d’écouter l’évènement kernel.response qui va arriver grâce au dispatcher d’évènements, afin de déclencher la redirection uniquement lorsque nous en avons besoin.

Nous allons créer notre listener dans KeiruaProd\FooBundle\Listener\LoginRedirectionListener.php, et y mettre le code suivant.

<br /> <?php namespace KeiruaProd\FooBundle\Listener;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Routing\Router; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\KernelEvents;

class LoginRedirectionListener {
private $router; private $dispatcher;

public function __construct(Router $router, EventDispatcher $dispatcher)
{
	// On enregistre les services dont on a besoin
    $this->router = $router;&lt;br />
    $this->dispatcher = $dispatcher;&lt;br />
}&lt;/p> &lt;p>	public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)&lt;br />
{&lt;br />
	// On demande a écouter une fois l'évènement kernel.response&lt;br />
    $this->dispatcher->addListener(KernelEvents::RESPONSE, array($this, 'redirectUserToProfilePage'));&lt;br />
}&lt;/p> &lt;p>    public function redirectUserToProfilePage(FilterResponseEvent $event)&lt;br />
{&lt;br />
	// on effectue la redirection&lt;br />
	$response = new RedirectResponse($this->router->generate('KeiruaProdFooBundle_myprofile'));&lt;br />
    $event->setResponse($response);&lt;br />
}&lt;br /> }&lt;br /> </code>

Ce code fait tout ce qui a été décrit plus haut : il demande à écouter le message kernel.response lorsque la méthode onSecurityInteractiveLogin est appelée. redirectUserToProfilePage se charge de rediriger l’utilisateur vers sa page de profil. A noter au passage l’utilisation du service de routage et du dispatcher d’évènements, que nous allons devoir injecter.

C’est bien beau tout ça, mais pour le moment, ce code n’est pas executé, car il n’y a aucune raison que la méthode onSecurityInteractiveLogin soit appelée. Il faut maintenant la reférencer, afin que symfony appelle lorsque c’est nécessaire. Il suffit de créer un service grâce à un fichier KeiruaProd\FooBundle\Ressources\config\loginlistener.xml, qui contient ce qui suit :

<br /> <br /> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <parameters> <parameter key="kernel.listener.keiruaprod.login.class"></parameter> </parameters> <services><br /> <service id="kernel.listener.keiruaprod.login" class="KeiruaProd\HabitsBundle\Listener\LoginRedirectionListener" scope="request"></p> <p> <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" /></p> <p> <argument type="service" id="router" /><br /> <argument type="service" id="event_dispatcher" /><br /> </service><br /> </services><br /> </container><br />

Ce fichier de configuration crée un service, que nous allons par la suite injecter dans le container de services.

Les tags permettent de préciser que ce service écoute les évènements, de dire lesquels et de préciser quelle méthode appeler. Il y a un paquet de noms de tags, référez vous à la documentation pour en savoir plus. En l’occurence on crée un listener qui, lors de l’évènement security.interactive_login, demande à appeler la méthode « onSecurityInteractiveLogin ». La section « argument » permet de préciser les arguments à passer au constructeur. Ici nous fournissons au constructeur le service de routage, référencé par son identifiant « router », ainsi que le service de dispatcher d’évènements. Au passage, si vous cherchez le nom d’un service, n’oubliez pas la commande app/console container:debug !

Nous avons configuré notre service. Plus qu’à le référencer dans l’application !

Ajoutez, dans la section imports de votre fichier app/config.yml une ligne du genre :
<br /> imports:<br /> - { resource: "@KeiruaProdFooBundle/Resources/config/loginlistener.xml" }<br />

Et voila ! Mission accomplie, on est maintenant capable de rediriger l’utilisateur vers sa page de profil quand il se connecte, que ce soit via FOSUserBundle ou par une autre méthode.