Versions

11/02/2014 : typos
13/06/2013 : Création

Contactez-nous

Kitpages
17 rue de la Frise
38000 Grenoble
tel : 04 58 00 33 81

Par Philippe Le Van (twitter accountplv) Dernière mise à jour : 11 February 2014

Créer des évènements dans Symfony, listener, subscriber, dispatcher

Introduction

Si vous voulez de la théorie, cherchez dans Google "pattern observer". Je vais plutôt vous parler de pratique.

Prenons un bundle Symfony fictif : AcmeUploadBundle servant à gérer des upload de fichiers.

On va utiliser des évènements pour gérer des traitements sur ces fichiers après l'upload (redimentionnement d'image, encodage divers, ...)

Les étapes

En pratique on aura les étapes suivantes :

  • On crée une classe Event qui contient le chemin complet d'un fichier
  • Dans l'action uploadAction d'un controller on fait les actions suivantes :
    • on gère l'upload
    • on envoie un évènement contenant le chemin du fichier uploadé

Ensuite on crée un listener, c'est à dire une classe qui va écouter l'évènement envoyé et qui peut faire des traitements

  • On crée une classe ResizeManager qui va écouter l'évènement
  • dès que l'évènement est envoyé la méthode resizeCallback($event) est appelée :
    • Elle extrait de $event le nom du fichier
    • Elle redimentionne l'image

Quel est l'intérêt de ça ?

On aurait pu mettre le redimensionnement directement dans le controller, pourquoi utiliser ces évènements ?

  • Si on veut ajouter un 2e traitement aux images (corriger les couleurs par exemple), on n'a pas à toucher au controller
  • Les classes de traitement des images (resize, autoAjustColor,...) sont complètement indépendantes du controller, réutilisables ailleurs et très ciblées sur un traitement donné

L'envoi de l'évènement

Créer la liste des évènements

Mon bundle AcmeUploadBundle peut renvoyer plusieurs évènements (chaque évènement a un nom). La convention dans Symfony2 consiste à les réunir dans une classe AcmeUploadEvents (dans notre exemple on n'a qu'un évènement).

<?php
namespace Acme\UploadBundle;

final class AcmeUploadEvents
{
    // chaque constante correspond à un évènement
    const AFTER_FILE_UPLOAD = "acme_upload.after_file_upload";
}

Créer l'objet évènement

Cet objet permet de transmettre des données aux différentes méthodes qui écoutent l'évènement. Notez que cet objet peut également être modifié par les listeners.

<?php
namespace Acme\UploadBundle\Event;

use Symfony\Component\EventDispatcher\Event;

class UploadEvent extends Event
{
    /** @var string */
    protected $fileName = null;

    public function setFileName($fileName)
    {
        $this->fileName = $fileName;
    }

    public function getFileName()
    {
        return $this->fileName;
    }
}

Gérer l'upload dans le controller

Dans le controller, on gère l'upload (move_upload_file & co) et après l'upload, on envoie l'évènement en passant par le dispatcher. Ce dispatcher est un service Symfony2 auquel on accède par $this->get("dispatcher").

<?php
namespace Acme\UploadBundle\Controller;

use Acme\UploadBundle\Event\UploadEvent;
use Acme\UploadBundle\AcmeUploadEvents;

class UploaderController extends Controller
{
    /**
     * @Route("/upload")
     */
    public function uploadAction()
    {
        // [...]
        // gestion de l'upload
        $tmp_name = $_FILES["pictures"]["tmp_name"][$key];
        $name = $_FILES["pictures"]["name"][$key];
        move_uploaded_file($tmp_name, "$uploads_dir/$name");
        $event = new UploadEvent();
        $event->setFileName("$uploads_dir/$name");

        // le dispatcher est un service symfony qui envoie l'event
        $this->get("dispatcher")->dispatch(
            AcmeUploadEvents::AFTER_FILE_UPLOAD, $event
        );
        // [...]
    }
}

Les listeners / subscribers

Créer un subscriber

Un subscriber est une classe qui écoute un ou plusieurs évènements

<?php
namespace Acme\UploadBundle\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Acme\UploadBundle\AcmeUploadEvents;
use Acme\UploadBundle\Event\UploadEvent;

class UploadSubscriber
    implements EventSubscriberInterface
{
    public function __construct()
    {
    }

    public static function getSubscribedEvents()
    {
        // Liste des évènements écoutés et méthodes à appeler
        return array(
            AcmeUploadEvents::AFTER_FILE_UPLOAD => 'resizeMethod'
        );
    }

    public function resizeMethod(UploadEvent $event)
    {
        $fileName = $event->getFileName();
        // [...] resize of the image
    }
}

Enregistrer le subscriber

Le subscriber peut ensuite être enregistré simplement dans l'injecteur de dépendance par exemple dans le fichier config.yml ou dans le fichier service.xml du bundle.

<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">

    <services>
        <service id="acme_upload.upload_subscriber" class="Acme\UploadBundle\EventListener\UploadSubscriber">
            <tag name="kernel.event_subscriber" />
        </service>
    </services>
</container>

Méthodes alternatives pour s'abonner à un event

Les subscribers sont la méthode la plus académique en Symfony2 pour s'abonner à un évènement.

Il existe cependant d'autres méthodes. Elles marchent aussi bien. Il n'y a ni avantages ni défauts. C'est plus un problème de "sensibilité syntaxique".

Je vous présente 2 de ces méthodes ci-dessous.

Un listener au lieu du subscriber

Autant un subscriber doit implémenter une SubscriberInterface, autant une classe listener est une class PHP standard qu'on enregistre comme un service dans le DIC. C'est l'enregistrement dans le DIC qui indique quel évènement est écouté et quel méthode doit être appelée.

<?php
namespace Acme\UploadBundle\EventListener;
use Acme\UploadBundle\Event\UploadEvent;
class MyListener
{
    public function cropImage(UploadEvent $event)
    {
        $fileName = $event->getFileName();
        // je fais mon traitement
    }
}

Et j'enregistre mon listener dans le DIC par exemple avec le service.xml.

<service id="acme_upload.crop" class="Acme\UploadBundle\EventListener\MyListener">
            <tag name="kernel.event_listener" event="acme_upload.after_file_upload" method="cropImage" />
        </service>

Un listener sans passer par le DIC

Parfois on a besoin d'un listener simple et on n'a pas envie de passer par un service et par le DIC. Voilà un exemple depuis un controller :

// dans un controller on pourrait avoir le code suivant :
$dispatcher = $this->get("dispatcher");
$dispatcher->addListener(
    'acme_upload.after_file_upload',
    function ($event) {
        // traitement à faire
    }
);

Note sur les tests unitaires et les events

Les listeners sont en général très indépendants. Ils se testent a priori sans problème.

En revanche, si un service envoie des évènements, ces évènements peuvent avoir des incidences fortes sur le fonctionnement du service. Dans vos tests unitaires, NE MOCKEZ PAS VOTRE DISPATCHER. Créez un subscriber de test, instanciez le dispatcher de symfony (\Symfony\Component\EventDispatcher\EventDispatcher) et injectez le.

Il faut tester cette "tuyauterie d'évènement" parce qu'elle fait partie de la logique de votre code. Vos tests du coup ne sont plus strictement unitaires, mais dans le cas des events, des tests strictement unitaires sont souvent moins pertinents.

(notons que ce point est un grand sujet de débats, mais si quelqu'un soutient le contraire, il a tort :-) )

Conclusion

Ajoutons qu'on peut définir des priorités pour ses listeners pour contrôler l'ordre d'exécution des listeners d'un évènement.

On peut imaginer des architectures puissantes et complexes avec évènements.

Le propos de ce tutoriel était surtout de montrer comment utiliser les events symfony2 de façon très académique.

N'hésitez pas à m'envoyer vos remarques en commentaires.

Commentaires

Ajouter un commentaire
UploadSubscriber
J'ai gardé le même exemple de cet article, la classe UploadSubscriber n'arrive pas à écouter l’événement envoyé : j'ai bien l’événement créé et le dispatcher déclenché mais le listener ne fait rien il n'est même pas appelé.
Re: SF2
Je trouve trés bien fait ce tutoriel, juste pour les abréviations tel que DIC ça fait pas du mal s'il y'avais une commentaire entre parenthèse.

Merci encore
Re: SF2
Pas de source dans github. Ce sont juste quelques exemples de code pour illustrer l'article. Il n'y a pas de source complet derrière.
SF2
vous avez pas le code source sur github  ?
Merci pour l'article
Article vraiment bien fait, c'est facile de trouver des ressources pour les EventListeners, mais les subscribers c'est pas aussi facile. Ca m'a aidé dans la construction de mon application !
Re: version
Ca marche au moins depuis symfony 2.3.
version
oui merci, ça j'ai vu ^^.

Mais j'aimerais la version exacte que vous avez utilisé, 2.1, 2.2 etc...
Re: version symfony peut être ?
Symfony 2. Si tu regardes la nav du site, on est dans la rubrique générale Symfony 2 :-)
version
version symfony peut être ?