• 30 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

Paperback available in this course

course.header.alt.is_certifying

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 9/4/17

Le gestionnaire d'évènements de Symfony2

Log in or subscribe for free to enjoy all this course has to offer!

N'avez-vous jamais rêvé d'exécuter un certain code à chaque page consultée de votre site Internet ? D'enregistrer chaque connexion des utilisateurs dans une base de données ? De modifier la réponse retournée au visiteur selon certains critères ? Eh bien, les développeurs de Symfony2 ne se sont pas contentés d'en rêver, ils l'ont fait !

En effet, comment réaliseriez-vous les cas précédents ? Avec unifsauvagement placé au fin fond d'un fichier ? Allons, un peu de sérieux, avec Symfony2 on va passer à la vitesse supérieure, et utiliser ce qu'on appelle le gestionnaire d'évènements.

Des évènements ? Pour quoi faire ?

Qu'est-ce qu'un évènement ?

Un évènement correspond à un moment clé dans l'exécution d'une page. Il en existe plusieurs, par exemple l'évènementkernel.requestqui est déclenché avant que le contrôleur ne soit exécuté. Cet évènement est déclenché à chaque page, mais il en existe d'autres qui ne le sont que lors d'actions particulières, par exemple l'évènementsecurity.interactive_login, qui correspond à l'identification d'un utilisateur.

Tous les évènements sont déclenchés à des endroits stratégiques de l'exécution d'une page Symfony2, et vont nous permettre de réaliser nos rêves de façon classe et surtout découplée.

Si vous avez déjà fait un peu de JavaScript, alors vous avez sûrement déjà traité des évènements. Par exemple, l'évènementonClickdoit vous parler. Il s'agit d'un évènement qui est déclenché lorsque l’utilisateur clique quelque part, et on y associe une action (quelque chose à faire). Bien sûr, en PHP vous ne serez pas capables de détecter le clic utilisateur, c'est un langage serveur ! Mais l'idée est exactement la même.

Qu'est-ce que le gestionnaire d'évènements ?

J'ai parlé à l'instant de découplage. C'est la principale raison de l'utilisation d'un gestionnaire d'évènements ! Par code découplé, j'entends que celui qui écoute l'évènement ne dépend pas du tout de celui qui déclenche l'évènement. Je m'explique :

  • On parle de déclencher un évènement lorsqu'on signale au gestionnaire d'évènements : « Tel évènement vient de se produire, préviens tout le monde, s'il-te-plaît. » Pour reprendre l'exemple de l'évènementkernel.request, c'est, vous l'aurez deviné, le Kernel qui déclenche l'évènement.

  • On parle d'écouter un évènement lorsqu'on signale au gestionnaire d'évènements : « Je veux que tu me préviennes dès que tel évènement se produira, s'il-te-plaît. »

Ainsi, lorsqu'on écoute un évènement que le Kernel va déclencher, on ne touche pas au Kernel, on ne vient pas perturber son fonctionnement. On se contente d'exécuter du code de notre côté, en ne comptant que sur le déclenchement de l'évènement ; c'est le rôle du gestionnaire d'évènements de nous prévenir. Le code est donc totalement découplé, et en tant que bons développeurs, on aime ce genre de code !

Au niveau du vocabulaire, un service qui écoute un évènement s'appelle un listener (personne qui écoute, en français).

Pour bien comprendre le mécanisme, je vous propose un schéma sur la figure suivante montrant les deux étapes :

  • Dans un premier temps, des services se font connaître du gestionnaire d'évènements pour écouter tel ou tel évènement. Ils deviennent des listeners ;

  • Dans un deuxième temps, quelqu'un (qui que ce soit) déclenche un évènement, c'est-à-dire qu'il prévient le gestionnaire d'évènements qu'un certain évènement vient de se produire. À partir de là, le gestionnaire d'évènements exécute chaque service qui s'est préalablement inscrit pour écouter cet évènement précis.

Fonctionnement du gestionnaire d'évènements
Fonctionnement du gestionnaire d'évènements

Écouter les évènements

Notre exemple

Dans un premier temps, nous allons apprendre à écouter des évènements. Pour cela, je vais me servir d'un exemple simple : l'ajout d'une bannière « bêta » sur notre site, qui est encore en bêta car nous n'avons pas fini son développement ! L'objectif est donc de modifier chaque page retournée au visiteur pour ajouter cette balise (dans le titre par exemple).

L'exemple est simple, mais vous montre déjà le code découplé qu'il est possible de faire. En effet, pour afficher un « bêta » sur chaque page, il suffirait d'ajouter un ou plusieurs petitsifdans la vue. Mais ce ne serait pas très joli, et le jour où votre site passe en stable il ne faudra pas oublier de retirer l'ensemble de cesif, bref, il y a un risque. Avec la technique d'un listener unique, il suffira de désactiver celui-ci.

Créer un service et son listener

Nous voulons effectuer une certaine action lors d'un certain évènement. Dans ce que nous voulons, il y a deux choses à distinguer :

  • D'une part, l'action à réaliser effectivement. Dans notre cas, il s'agit d'ajouter une mention beta à une réponse contenant du HTML. C'est une action certes simple, mais une action quand même, qui mérite son propre objet : un simple service. Appelons-leBetaHTML car il ajoute la mention beta à du HTML.

  • D'autre part, le fait d'exécuter l'action précédente à un certain moment, avec certains paramètres. C'est une autre action, qui mérite donc un autre objet : le listener. Appelons-leBetaListener.

C'est pourquoi nous allons créer deux objets différents.

Attaquons le premier objet, celui qui contient la logique de ce qu'il y a à faire : ajouter la mention beta à une réponse.

 

Pour savoir où placer ce service dans notre bundle, il faut se poser la question suivante : « À quelle fonction répond mon service ? » La réponse est « À définir la version bêta », on va donc placer l'objet dans le répertoireBeta, tout simplement. Pour l'instant il sera tout seul dans ce répertoire, mais on y ajoutera plus tard le listener.

Je vous invite donc à créer cette classe :

<?php
// src/OC/PlatformBundle/Beta/BetaHTML.php

namespace OC\PlatformBundle\Beta;

use Symfony\Component\HttpFoundation\Response;

class BetaHTML
{
  // Méthode pour ajouter le « bêta » à une réponse
  public function displayBeta(Response $response, $remainingDays)
  {
    $content = $response->getContent();

    // Code à rajouter
    $html = '<span style="color: red; font-size: 0.5em;"> - Beta J-'.(int) $remainingDays.' !</span>';

    // Insertion du code dans la page, dans le premier <h1>
    $content = preg_replace(
      '#<h1>(.*?)</h1>#iU',
      '<h1>$1'.$html.'</h1>',
      $content,
      1
    );

    // Modification du contenu dans la réponse
    $response->setContent($content);

    return $response;
  }
}

Ainsi que la configuration du service correspondant :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.beta.html:
        class: OC\PlatformBundle\Beta\BetaHTML

Pour l'instant, c'est un service tout simple, qui n'écoute personne. On dispose d'une méthodedisplayBetaprête à l'emploi pour modifier la réponse lorsqu'on la lui donnera.

Passons maintenant à la création du listener à proprement parler. Il nous faut donc une autre classe, qui contient une méthode pour ajouter si besoin (en fonction de la date) la mention beta à la réponse courante. Voici pour l'instant le squelette de cette classe, on n'a pas encore tout le contenu de cette méthode :

<?php
// src/OC/PlatformBundle/Beta/BetaListener.php

namespace OC\PlatformBundle\Beta;

use Symfony\Component\HttpFoundation\Response;

class BetaListener
{
  // Notre processeur
  protected $betaHTML;

  // La date de fin de la version bêta :
  // - Avant cette date, on affichera un compte à rebours (J-3 par exemple)
  // - Après cette date, on n'affichera plus le « bêta »
  protected $endDate;

  public function __construct(BetaHTML $betaHTML, $endDate)
  {
    $this->betaHTML = $betaHTML;
    $this->endDate  = new \Datetime($endDate);
  }

  public function processBeta()
  {
    $remainingDays = $this->endDate->diff(new \Datetime())->format('%d');

    if ($remainingDays <= 0) {
      // Si la date est dépassée, on ne fait rien
      return;
    }
    
    // Ici on appelera la méthode
    // $this->betaHTML->displayBeta()
  }
}

Voici donc le rôle de tout listener : un objet capable de décider s'il faut ou non appeler un autre objet qui remplira une certaine fonction. La fonction du listener n'est que de décider quand appeler l'autre objet.

Dans notre cas, la décision ou non d'appeler le BetaHTML repose sur la date courante : si elle est antérieure à la date définie dans le constructeur, on exécute BetaHTML. Sinon, on ne fait rien.

 

Écouter un évènement

Vous le savez maintenant, pour que notre classe précédente écoute quelque chose, il faut la présenter au gestionnaire d'évènements. Il existe deux manières de le faire : manipuler directement le gestionnaire d'évènements, ou passer par les services.

Je ne vous cache pas qu'on utilisera très rarement la première méthode, mais je vais vous la présenter en premier, car elle permet de bien comprendre ce qu'il se passe dans la deuxième.

Méthode 1 : Manipuler directement le gestionnaire d'évènements

Cette première méthode, un peu brute, consiste à passer notre objetBetaListenerau gestionnaire d'évènements. Ce gestionnaire existe en tant que service sous Symfony, il s'agit de l'EventDispatcher. Concrètement, voici comment faire :

<?php
// Depuis un contrôleur

use OC\PlatformBundle\Beta\BetaListener;

// …

// On instancie notre listener
$betaListener = new BetaListener('2014-10-20');

// On récupère le gestionnaire d'évènements, qui heureusement est un service !
$dispatcher = $this->get('event_dispatcher');

// On dit au gestionnaire d'exécuter la méthode onKernelResponse de notre listener
// Lorsque l'évènement kernel.response est déclenché
$dispatcher->addListener(
  'kernel.response',
  array($betaListener, 'processBeta')
);

À partir de maintenant, dès que l'évènementkernel.responseest déclenché, le gestionnaire d'évènements exécutera la méthode $betaListener->processBeta().

Méthode 2 : Définir son listener comme service

Comme je vous l'ai dit, c'est cette méthode qu'on utilisera 99 % du temps. Elle est beaucoup plus simple et permet d'éviter le problème d'évènement qui se produit avant l'enregistrement de votre listener dans le gestionnaire d'évènements.

Mettons en place cette méthode pas à pas. Tout d'abord, définissez votre listener en tant que service, comme ceci :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.beta.listener:
        class: OC\PlatformBundle\Beta\BetaListener
        arguments: [@oc_platform.beta.html, "2013-10-20"]

À partir de maintenant, votre listener est accessible via le conteneur de services. Pour aller plus loin, il faut définir le tagkernel.event_listenersur ce service. Le processus est le suivant : une fois le gestionnaire d'évènements instancié par le conteneur de services, il va récupérer tous les services qui ont ce tag, et exécuter le code de la méthode 1 qu'on vient de voir afin d'enregistrer les listeners dans lui-même. Tout se fait automatiquement !

Voici donc le tag en question à rajouter à notre service :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.beta.listener:
        class: OC\PlatformBundle\Beta\BetaListener
        arguments: [@oc_platform.beta.html, "2013-10-20"]
        tags:
            - { name: kernel.event_listener, event: kernel.response, method: processBeta }

Il y a deux paramètres à définir dans le tag, qui sont les deux paramètres qu'on a utilisés précédemment dans la méthode$dispatcher->addListener():

  • event: c'est le nom de l'évènement que le listener veut écouter ;

  • method: c'est le nom de la méthode du listener à exécuter lorsque l'évènement est déclenché.

C'est tout ! Avec uniquement cette définition de service et le bon tag associé, votre listener sera exécuté à chaque déclenchement de l'évènementkernel.response!

Bien entendu, votre listener peut tout à fait écouter plusieurs évènements. Il suffit pour cela d'ajouter un autre tag avec des paramètres différents. Voici ce que cela donnerait si on voulait écouter l'évènementkernel.controller:

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.beta.listener:
        class: OC\PlatformBundle\Beta\BetaListener
        arguments: [@oc_platform.beta.html, "2013-10-20"]
        tags:
            - { name: kernel.event_listener, event: kernel.response, method: processBeta }
            - { name: kernel.event_listener, event: kernel.controller, method: ignoreBeta }

Maintenant, passons à cette fameuse méthodeonKernelResponse.

Création de la méthode à exécuter du listener

Vous distinguez bien les deux points dont je vous parlais. D'un côté, on a notre service BetaHTML qui réalise la fonction "ajouter une mention beta". Il remplit parfaitement sa fonction. De l'autre côté, on a la solution technique pour exécuter la fonction précédente lorsqu'on le veut, c'est notre listener BetaListener. C'est lui qui est enregistré dans le gestionnaire d'évènements grâce au tag, et c'est lui qui décide quand exécuter la fonction.

Le listener permet également de passer les bons arguments à notre service BetaHTML. En effet, lorsque le gestionnaire d'évènements exécute ses listeners, il ne se préoccupe pas de leurs arguments ! Le seul argument qu'il leur donne est un objet Symfony\Component\EventDispatcher\Event, représentant l'évènement en cours.

Dans notre cas de l'évènementkernel.response, on a le droit à un objetSymfony\Component\HttpKernel\Event\FilterResponseEvent, qui hérite bien évidemment du premier.

Dans notre cas, l'évènementFilterResponseEventdispose des méthodes suivantes :

<?php
class FilterResponseEvent
{
  public function getResponse();
  public function setResponse(Response $response);
  public function getKernel();
  public function getRequest();
  public function getRequestType();
  public function isPropagationStopped();
  public function stopPropagation();
}

Ce sont surtout les méthodesgetResponse()etsetResponsequi vont nous être utiles pour notre BetaListener : elles permettent respectivement de récupérer la réponse et de la modifier, c'est exactement ce que l'on veut !

On a maintenant toutes les informations nécessaires, il est temps de construire réellement la méthodeprocessBeta de notre listener. Tout d'abord, voici le principe général pour ce type de listener qui vient modifier une partie de l'évènement (ici, la réponse) :

<?php
// src/OC/PlatformBundle/Beta/BetaListener.php

namespace OC\PlatformBundle\Beta;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class BetaListener
{
  // L'argument de la méthode est un FilterResponseEvent
  public function processBeta(FilterResponseEvent $event)
  {
    // On teste si la requête est bien la requête principale (et non une sous-requête)
    if (!$event->isMasterRequest()) {
      return;
    }

    // On récupère la réponse que le gestionnaire a insérée dans l'évènement
    $response = $event->getResponse();

    // Ici on modifie comme on veut la réponse…

    // Puis on insère la réponse modifiée dans l'évènement
    $event->setResponse($response);
  }
}

Adaptons maintenant cette base à notre exemple, il suffit juste de rajouter l'appel à notre service BetaHTML, voyez par vous-mêmes :

<?php
// src/OC/PlatformBundle/Beta/BetaListener.php

namespace OC\PlatformBundle\Beta;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class BetaListener
{
  public function processBeta(FilterResponseEvent $event)
  {
    if (!$event->isMasterRequest()) {
      return;
    }

    $remainingDays = $this->endDate->diff(new \Datetime())->format('%d');

    // Si la date est dépassée, on ne fait rien
    if ($remainingDays <= 0) {
      return;
    }

    // On utilise notre BetaHRML
    $response = $this->betaHTML->displayBeta($event->getResponse(), $remainingDays);
    // On met à jour la réponse avec la nouvelle valeur
    $event->setResponse($response);
  }
}

Voilà, votre listener est maintenant opérationnel ! Actualisez n'importe quelle page de votre site et vous verrez la mention « bêta » apparaître comme sur la figure suivante.

La mention « bêta » apparaît
La mention « bêta » apparaît

Ici la bonne exécution de votre listener est évidente, car on a modifié l'affichage, mais parfois rien n'est moins sûr. Pour vérifier que votre listener est bien exécuté, allez dans l'ongletEventsdu Profiler, vous devez l'apercevoir dans le tableau visible à la figure suivante.

Notre listener figure dans la liste des listeners exécutés
Notre listener figure dans la liste des listeners exécutés

Méthodologie

Vous connaissez maintenant la syntaxe pour créer un listener qui va écouter un évènement. Vous avez pu constater que le principe est assez simple, mais pour rappel voici la méthode à appliquer lorsque vous souhaitez écouter un évènement :

  • Tout d'abord, créez un service qui va remplir la fonction que vous souhaitez. Si vous avez un service déjà existant, cela va très bien.

  • Ensuite, choisissez bien l'évènement que vous devez écouter. On a pris directement l'évènementkernel.responsepour l'exemple, mais vous devez choisir correctement le vôtre dans la liste que je dresse plus loin.

  • Puis créez une classe (le future listener) qui contient une méthode qui va faire le lien entre le déclenchement de l'évènement et le code que vous voulez exécuter (le service précédent). Il s'agit de la méthodeprocessBeta que nous avons utilisée.

  • Enfin, définissez votre classe comme un service (sauf si c'en était déjà un), et ajoutez à la définition du service le bon tag pour que le gestionnaire d'évènements retrouve votre listener.

Les évènements Symfony2… et les nôtres !

Symfony2 déclenche déjà quelques évènements dans son processus interne. Mais il sera bien évidemment possible de créer puis déclencher nos propres évènements !

Les évènements Symfony2

L'évènementkernel.request

Cet évènement est déclenché très tôt dans l'exécution d'une page, avant même que le choix du contrôleur à exécuter ne soit fait. Son objectif est de permettre à un listener de retourner immédiatement une réponse, sans même passer par l'exécution d'un contrôleur donc. Il est également possible de définir des attributs dans la requête. Dans le cas où un listener définit une réponse, alors les listeners suivants ne seront pas exécutés ; on reparle de la priorité des listeners plus loin.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estGetResponseEvent, dont les méthodes sont les suivantes :

<?php

class GetResponseEvent
{
  public function getResponse();
  public function setResponse(Response $response);
  public function hasResponse();
  public function getKernel();
  public function getRequest();
  public function getRequestType();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}
L'évènementkernel.controller

Cet évènement est déclenché après que le contrôleur à exécuter a été défini, mais avant de l'exécuter effectivement. Son objectif est de permettre à un listener de modifier le contrôleur à exécuter. Généralement, c'est également l'évènement utilisé pour exécuter du code sur chaque page.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estFilterControllerEvent, dont les méthodes sont les suivantes :

<?php

class FilterControllerEvent
{
  public function getController();
  public function setController($controller);
  public function getKernel();
  public function getRequest();
  public function getRequestType();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}

Voici comment utiliser cet évènement depuis un listener pour modifier le contrôleur à exécuter sur la page en cours :

<?php

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

public function onKernelController(FilterControllerEvent $event)
{
  // Vous pouvez récupérer le contrôleur que le noyau avait l'intention d'exécuter
  $controller = $event->getController();

  // Ici vous pouvez modifier la variable $controller, etc.
  // $controller doit être de type PHP callable

  // Si vous avez modifié le contrôleur, prévenez le noyau qu'il faut exécuter le vôtre :
  $event->setController($controller);
}
L'évènementkernel.view

Cet évènement est déclenché lorsqu'un contrôleur n'a pas retourné d'objetResponse. Son objectif est de permettre à un listener d'attraper le retour du contrôleur (s'il y en a un) pour soit construire une réponse lui-même, soit personnaliser l'erreur levée.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estGetResponseForControllerResultEvent(rien que ça !), dont les méthodes sont les suivantes :

<?php

class GetResponseForControllerResultEvent
{
  public function getControllerResult();
  public function getResponse();
  public function setResponse(Response $response);
  public function hasResponse();
  public function getKernel();
  public function getRequest();
  public function getRequestType();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}

Voici comment utiliser cet évènement depuis un listener pour construire une réponse à partir du retour du contrôleur de la page en cours :

<?php

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;

public function onKernelView(GetResponseForControllerResultEvent $event)
{
  // Récupérez le retour du contrôleur (ce qu'il a mis dans son « return »)
  $val = $event->getControllerResult();

  // Créez une nouvelle réponse
  $response = new Response();

  // Construisez votre réponse comme bon vous semble…

  // Définissez la réponse dans l'évènement, qui la donnera au noyau qui, finalement, l'affichera
  $event->setResponse($response);
}
L'évènementkernel.response

Cet évènement est déclenché après qu'un contrôleur a retourné un objetResponse; c'est celui que nous avons utilisé dans notre exemple de listener. Son objectif, comme vous avez pu vous en rendre compte, est de permettre à un listener de modifier la réponse générée par le contrôleur avant de l'envoyer à l'internaute.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estFilterResponseEvent, dont les méthodes sont les suivantes :

<?php

class FilterResponseEvent
{
  public function getControllerResult();
  public function getResponse();
  public function setResponse(Response $response);
  public function hasResponse();
  public function getKernel();
  public function getRequest();
  public function getRequestType();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}
L'évènementkernel.exception

Cet évènement est déclenché lorsqu'une exception est levée. Son objectif est de permettre à un listener de modifier la réponse à renvoyer à l'internaute, ou bien de modifier l'exception.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estGetResponseForExceptionEvent, dont les méthodes sont les suivantes :

<?php

class GetResponseForExceptionEvent
{
  public function getException();
  public function setException(\Exception $exception);
  public function getResponse();
  public function setResponse(Response $response);
  public function hasResponse();
  public function getKernel();
  public function getRequest();
  public function getRequestType();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}
L'évènementsecurity.interactive_login

Cet évènement est déclenché lorsqu'un utilisateur s'identifie via le formulaire de connexion. Son objectif est de permettre à un listener d'archiver une trace de l'identification, par exemple.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estSymfony\Component\Security\Http\Event\InteractiveLoginEvent, dont les méthodes sont les suivantes :

<?php

class InteractiveLoginEvent
{
  public function getAuthenticationToken();
  public function getRequest();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}
L'évènementsecurity.authentication.success

Cet évènement est déclenché lorsqu'un utilisateur s'identifie avec succès, quelque soit le moyen utilisé (formulaire de connexion, cookiesremember_me). Son objectif est de permettre à un listener d'archiver une trace de l'identification, par exemple.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estSymfony\Component\Security\Core\Event\AuthenticationEvent, dont les méthodes sont les suivantes :

<?php

class AuthenticationEvent
{
  public function getAuthenticationToken();
  public function getRequest();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}
L'évènementsecurity.authentication.failure

Cet évènement est déclenché lorsqu'un utilisateur effectue une tentative d'identification échouée, quelque soit le moyen utilisé (formulaire de connexion, cookiesremember_me). Son objectif est de permettre à un listener d'archiver une trace de la mauvaise identification, par exemple.

La classe de l'évènement donné en argument par le gestionnaire d'évènements estSymfony\Component\Security\Core\Event\AuthenticationFailureEvent, dont les méthodes sont les suivantes :

<?php

class AuthenticationFailureEvent
{
  public function getAuthenticationException();
  public function getRequest();
  public function getDispatcher();
  public function isPropagationStopped();
  public function stopPropagation();
}

Créer nos propres évènements

Les évènements Symfony2 couvrent la majeure partie du process d'exécution d'une page, ou alors du process d'identification d'un utilisateur. Cependant, on aura parfois besoin d'appliquer cette conception par évènement à notre propre code, notre propre logique. Cela permet encore une fois de bien découpler les différentes fonctions de notre site.

Nous allons suivre un autre exemple pour la création d'un évènement : celui d'un outil de surveillance des messages postés, qu'on appelleraBigBrother. L'idée est d'avoir un outil qui permette de censurer les messages de certains utilisateurs et/ou de nous envoyer une notification lorsqu'ils postent des messages. L'avantage de passer par un évènement au lieu de modifier directement le contrôleur, c'est de pouvoir appliquer cet outil à plusieurs types de messages : les annonces, les candidatures, les messages sur le forum si vous en avez un, etc.

Pour reproduire le comportement des évènements, il nous faut trois étapes :

  • D'abord, définir la liste de nos évènements possibles. Il peut bien entendu y en avoir qu'un seul.

  • Ensuite, construire la classe de l'évènement. Il faut pour cela définir les informations qui peuvent être échangées entre celui qui émet l'évènement et celui qui l'écoute.

  • Enfin, déclencher l'évènement bien entendu.

Définir la liste de nos évènements

Nous allons définir une classe avec juste des constantes qui contiennent le nom de nos évènements. Cette classe est facultative en soi, mais c'est une bonne pratique qui nous évitera d'écrire directement le nom de l'évènement. On utilisera ainsi le nom de la constante, défini à un seul endroit, dans cette classe. J'appelle cette classeBigbrotherEvents, mais c'est totalement arbitraire, voici son code :

<?php
// src/OC/PlatformBundle/Bigbrother/BigbrotherEvents.php

namespace OC\PlatformBundle\Bigbrother;

final class BigbrotherEvents
{
  const onMessagePost = 'oc_platform.bigbrother.post_message';
  // Vos autres évènements…
}

Cette classe ne fait donc rien, elle ne sert qu'à faire la correspondance entreBigbrotherEvents::onMessagePostqu'on utilisera pour déclencher l'évènement et le nom de l'évènement en lui mêmeoc_platform.bigbrother.post_message.

Construire la classe de l'évènement

La classe de l'évènement, c'est, rappelez-vous, la classe de l'objet que le gestionnaire d'évènements va transmettre aux listeners. En réalité on ne l'a pas encore vu, mais c'est celui qui déclenche l'évènement qui crée une instance de cette classe. Le gestionnaire d'évènements ne fait que la transmettre, il ne la crée pas.

Voici dans un premier temps le squelette commun à tous les évènements. On va appeler le nôtreMessagePostEvent:

<?php
// src/OC/PlatformBundle/Bigbrother/MessagePostEvent.php

namespace OC\PlatformBundle\Bigbrother;

use Symfony\Component\EventDispatcher\Event;

class MessagePostEvent extends Event
{
}

C'est tout simplement une classe vide qui étend la classeEventdu composantEventDispatcher.

Ensuite, il faut rajouter la spécificité de notre évènement. On a dit que le but de la fonctionnalité BigBrother est de censurer le message de certains utilisateurs, on a donc deux informations à transmettre du déclencheur au listener : le message et l'utilisateur qui veut le poster. On doit donc rajouter ces deux attributs à l'évènement :

<?php
// src/OC/PlatformBundle/Bigbrother/MessagePostEvent.php

namespace OC\PlatformBundle\Bigbrother;

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Security\Core\User\UserInterface;

class MessagePostEvent extends Event
{
  protected $message;
  protected $user;

  public function __construct($message, UserInterface $user)
  {
    $this->message = $message;
    $this->user    = $user;
  }

  // Le listener doit avoir accès au message
  public function getMessage()
  {
    return $this->message;
  }

  // Le listener doit pouvoir modifier le message
  public function setMessage($message)
  {
    return $this->message = $message;
  }

  // Le listener doit avoir accès à l'utilisateur
  public function getUser()
  {
    return $this->user;
  }

  // Pas de setUser, le listener ne peut pas modifier l'auteur du message !
}

Faites attention aux getters et setters, vous devez les définir soigneusement en fonction de la logique de votre évènement :

  • Un getter doit tout le temps être défini sur vos attributs. Car si votre listener n'a pas besoin d'un attribut (ce qui justifierait l'absence de getter), alors l'attribut ne sert à rien !

  • Un setter ne doit être défini que si le listener peut modifier la valeur de l'attribut. Ici c'est le cas du message. Cependant, on interdit au listener de modifier l'auteur du message, cela n'aurait pas de sens.

Déclencher l'évènement

Déclencher et utiliser un évènement se fait assez naturellement lorsqu'on a bien défini l'évènement et ses attributs. Reprenons le code de l'action du contrôleurAdvertControllerqui permet d'ajouter une annonce. Voici comment on l'adapterait pour déclencher l'évènement avant l'enregistrement effectif de l'annonce :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use OC\PlatformBundle\Bigbrother\BigbrotherEvents;
use OC\PlatformBundle\Bigbrother\MessagePostEvent;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class AdvertController extends Controller
{
  public function addAction(Request $request)
  {
    // …

    if ($form->handleRequest($request)->isValid()) {
      // On crée l'évènement avec ses 2 arguments
      $event = new MessagePostEvent($advert->getContent(), $advert->getUser());

      // On déclenche l'évènement
      $this
        ->get('event_dispatcher')
        ->dispatch(BigbrotherEvents::onMessagePost, $event)
      ;

      // On récupère ce qui a été modifié par le ou les listeners, ici le message
      $advert->setContent($event->getMessage());

      $em = $this->getDoctrine()->getManager();
      $em->persist($advert);
      $em->flush();

      // …
    }
  }
}

C'est tout pour déclencher un évènement ! Vous n'avez plus qu'à reproduire ce comportement la prochaine fois que vous créerez une action qui permet aux utilisateurs d'ajouter un nouveau message (livre d'or, messagerie interne, etc.).

Écouter l'évènement

Comme vous avez pu le voir, on a déclenché l'évènement alors qu'il n'y a pas encore de listener. Cela ne pose pas de problème, bien au contraire : cela va nous permettre par la suite d'ajouter un ou plusieurs listeners qui seront alors exécutés au milieu de notre code. Ça, c'est du découplage !

Pour aller jusqu'au bout de l'exemple, voici ma proposition pour un listener. C'est juste un exemple, ne le prenez pas pour argent comptant. Voici d'abord le service qui agit :

<?php
// src/OC/PlatformBundle/Bigbrother/CensorshipProcessor.php

namespace OC\PlatformBundle\Bigbrother;

use Symfony\Component\Security\Core\User\UserInterface;

class CensorshipProcessor
{
  protected $mailer;

  public function __construct(\Swift_Mailer $mailer)
  {
    $this->mailer = $mailer;
  }

  // Méthode pour notifier par e-mail un administrateur
  public function notifyEmail($message, UserInterface $user)
  {
    $message = \Swift_Message::newInstance()
      ->setSubject("Nouveau message d'un utilisateur surveillé")
      ->setFrom('admin@votresite.com')
      ->setTo('admin@votresite.com')
      ->setBody("L'utilisateur surveillé '".$user->getUsername()."' a posté le message suivant : '".$message."'");

    $this->mailer->send($message);
  }

  // Méthode pour censurer un message (supprimer les mots interdits)
  public function censorMessage($message)
  {
    $message = str_replace(array('top secret', 'mot interdit'), '', $message);

    return $message;
  }
}

Ainsi que le listener à proprement parler, qui vient exécuter la censure seulement lorsque l'auteur du message est dans une liste pré-définie (ici, je la passe en argument du constructeur) :

<?php
// src/OC/PlatformBundle/Bigbrother/CensorshipListener.php

namespace OC\PlatformBundle\Bigbrother;

class CensorshipListener
{
  protected $processor;
  protected $listUsers = array();

  public function __construct(CensorshipProcessor $processor, $listUsers)
  {
    $this->processor = $processor;
    $this->listUsers = $listUsers;
  }

  public function processMessage(MessagePostEvent $event)
  {
    // On active la surveillance si l'auteur du message est dans la liste
    if (in_array($event->getUser()->getId(), $this->listUsers)) {
      // On envoie un e-mail à l'administrateur
      $this->processor->notifyEmail($event->getMessage(), $event->getUser());

      // On censure le message
      $message = $this->processor->censorMessage($event->getMessage());
      // On enregistre le message censuré dans l'event
      $event->setMessage($message);
    }
  }
}

Et bien sûr, la définition des services correspondants :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.censorship_processor:
        class:     OC\PlatformBundle\Bigbrother\CensorshipProcessor
        arguments: [@mailer]

    oc_platform.censorship_listener:
        class:     OC\PlatformBundle\Bigbrother\CensorshipListener
        arguments: [@oc_platform.censorship_processor, [1, 2, 3]]
        tags:
            - { name: kernel.event_listener, event: oc_platform.bigbrother.post_message, method: processMessage }

J'ai mis ici arbitrairement une liste[1, 2, 3]pour les id des utilisateurs à surveiller, mais vous pouvez personnaliser cette liste ou même la rendre dynamique.

Allons un peu plus loin

Le gestionnaire d'évènements est assez simple à utiliser, et vous connaissez en réalité déjà tout ce qu'il faut savoir. Mais je ne pouvais pas vous laisser sans vous parler de trois points supplémentaires, qui peuvent être utiles.

Étudions donc les souscripteurs d'évènements, qui peuvent se mettre à écouter un évènement de façon dynamique, l'ordre d'exécution des listeners, ainsi que la propagation des évènements.

Les souscripteurs d'évènements

Les souscripteurs sont assez semblables aux listeners. La seule différence est la suivante : au lieu d'écouter toujours le même évènement défini dans un fichier de configuration, un souscripteur peut écouter dynamiquement un ou plusieurs évènements.

Concrètement, c'est l'objet souscripteur lui-même qui va dire au gestionnaire d'évènements les différents évènements qu'il veut écouter. Pour cela, un souscripteur doit implémenter l'interfaceEventSubscriberInterface, qui ne contient qu'une seule méthode :getSubscribedEvents(). Vous l'avez compris, cette méthode doit retourner les évènements que le souscripteur veut écouter.

Voici par exemple comment on pourrait transformer notreCensorshipListener en un souscripteur :

<?php
// src/OC/PlatformBundle/Bigbrother/CensorshipListener.php

namespace OC\PlatformBundle\Bigbrother;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CensorshipListener implements EventSubscriberInterface
{
  // La méthode de l'interface que l'on doit implémenter, à définir en static
  static public function getSubscribedEvents()
  {
    // On retourne un tableau « nom de l'évènement » => « méthode à exécuter »
    return array(
      'oc_platform.bigbrother.post_message' => 'processMessage',
      'autre.evenement'                     => 'autreMethode',
    );
  }

  public function processMessage(MessagePostEvent $event)
  {
    // ...
  }

  public function autreMethode()
  {
    // ...
  }
}

Bien sûr, il faut ensuite déclarer ce souscripteur au gestionnaire d'évènements. Pour cela, ce n'est plus le tagkernel.event_listener qu'il faut utiliser, mais :kernel.event_subscriber. Avec ce tag, le gestionnaire d'évènements récupère tous les souscripteurs d'évènements et les enregistre.

Pas besoin d'ajouter les attributs event et method sur le tag, car c'est la méthodegetSubscribedEvents qui retourne ces informations :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.bigbrother.censorship_listener:
        class:     OC\PlatformBundle\Bigbrother\CensorshipListener
        arguments: [@oc_platform.bigbrother.censorship_processor, [1, 2, 3]]
        tags:
            - { name: kernel.event_subscriber }
L'ordre d'exécution des listeners

On peut définir l'ordre d'exécution des listeners grâce à un indicepriority. Cet ordre aura ainsi une importance lorsqu'on verra comment stopper la propagation d'un évènement.

La priorité des listeners

Vous pouvez ajouter un indice de priorité à vos listeners, ce qui permet de personnaliser leur ordre d'exécution sur un même évènement. Plus cet indice de priorité est élevé, plus le listener sera exécuté tôt, c'est-à-dire avant les autres. Par défaut, si vous ne précisez pas la priorité, elle est de 0.

Vous pouvez la définir très simplement dans le tag de la définition du service. Ici, je l'ai définie à2:

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.beta.listener:
        class: OC\PlatformBundle\Beta\BetaListener
        arguments: [@oc_platform.beta.html, "2013-10-20"]
        tags:
            - { name: kernel.event_listener, event: kernel.response, method: processBeta, priority: 2 }

Et pour les souscripteurs, voici comment adapter la méthodegetSubscribedEvents pour y ajouter l'information de la priorité. Ici j'ai mis une priorité de 2 également :

<?php
// Dans un souscripteur :

  static public function getSubscribedEvents()
  {
    return array(
      'oc_platform.bigbrother.post_message' => array('processMessage' => 2)
    );
  }

Vous pouvez également définir une priorité négative, ce qui aura pour effet d'exécuter votre listener relativement tard dans l'évènement. Je dis bien relativement, car s'il existe un autre listener avec une priorité de -128 alors que le vôtre est à -64, alors c'est lui qui sera exécuté après le vôtre.

La propagation des évènements

Si vous avez l'œil bien ouvert, vous avez pu remarquer que tous les évènements qu'on a vus précédemment avaient deux méthodes en commun :stopPropagation()etisPropagationStopped(). Eh bien, vous ne devinerez jamais, mais la première méthode permet à un listener de stopper la propagation de l'évènement en cours !

La conséquence est donc directe : tous les autres listeners qui écoutaient l'évènement et qui ont une priorité plus faible ne seront pas exécutés. D'où l'importance de l'indice de priorité que nous venons juste de voir !

Pour visualiser ce comportement, je vous propose de modifier légèrement notreBetaListener. Rajoutez cette ligne à la fin de sa méthodeonKernelResponse:

<?php
// src/OC/PlatformBundle/Beta/BetaListener.php

namespace OC\PlatformBundle\Beta;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class BetaListener
{
  public function processBeta(FilterResponseEvent $event)
  {
    // ...
    
    // On stoppe la propagation de l'évènement en cours (ici, kernel.response)
    $event->stopPropagation();
  }
}

Actualisez une page. Vous voyez une différence ? La barre d'outils a disparu du bas de la page ! En effet, cette barre est ajoutée avec un listener sur l'évènementkernel.response, exactement comme notre mention « bêta ». Or comme notre listener a une priorité plus élevé, et qu'il a stoppé la propagation de l'évènement, le listener de la barre d'outils n'a pas été exécuté. Pour votre culture, il s'agit deSymfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener.

En résumé

  • Un évènement correspond à un moment clé dans l'exécution d'une page ou d'une action.

  • On parle de déclencher un évènement lorsqu'on signale au gestionnaire d'évènements qu'un certain évènement vient de se produire.

  • On dit qu'un listener écoute un évènement lorsqu'on signale au gestionnaire d'évènements qu'il faut exécuter ce listener dès qu'un certain évènement se produit.

  • Un listener est une classe qui remplit une fonction, et qui écoute un ou plusieurs évènements pour savoir quand exécuter sa fonction.

  • On définit les évènements à écouter via les tags du service listener.

  • Il existe plusieurs évènements de base dans Symfony2, et il est possible de créer les nôtres.

Example of certificate of achievement
Example of certificate of achievement