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
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
Note : on ne peut plus ajouter de commentaire sur ce site