Navigation

Related Articles

Back to Latest Articles

Utilisation de l’event dispatcher depuis les classes du modèle.


Olivier
Utilisation de l’event dispatcher...

Le but de cet article n’est pas d’expliquer le fonctionnement des événements symfony, la documentation officielle est très bien faite à ce sujet, et pas mal d’articles expliquant leur implementation existent déjà.

Le but est juste de savoir comment faire pour avoir accès à l’event dispatcher depuis les classes du modèle, de manière à pouvoir lever des événements « métiers » qui permettent des traitements qui se rapprochent des trigger sql. L’avantage est de rester au sein de l’application symfony et ne pas disperser la logique. C’est une problèmatique que nous avons rencontré à plusieurs reprises dans les projets et nous avons pu trouver cette implementation grace à n1k0 de chez Akei.

(Pour la suite, je vais me servir d’une partie du modèle de Jobeet comme je l’avais fait dans mon post précédent)

A la base, l’event dispatcher se trouve dans le contexte, et donc accessible principalement depuis les actions des modules. L’idée est de le rendre accessible aux classes du modèle doctrine de manière à pouvoir notifier des événements « métier » directement depuis les méthodes. On va donc passer l’event dispatcher à la classe de base dont hérite toutes les classes du modèle. Ou plus exactement, on va rajouter une classe custom dans la pile d’héritage des classes du modèle.

On créé donc une classe CustomDoctrineRecord, que je place dans /lib/model/doctrine/record/CustomDoctrineRecord.class.php pour éviter qu’elle se retrouve noyée avec les classes du modèle. On va faire passer à cette classe l’event dispatcher par des méthodes statiques.
Les classes de base du modèle héritent de sfDoctrineRecord, il faut donc impérativement que la classe custom hérite elle aussi de sfDoctrineRecord.

Fichier : /lib/model/doctrine/record/CustomDoctrineRecord.class.php

<?php
 
abstract class CustomDoctrineRecord extends sfDoctrineRecord
{
  static protected $dispatcher = null;
 
  /**
   * Sets the EventDispatcher
   *
   * @param sfEventDispatcher $dispatcher 
   */
  static public function setEventDispatcher(sfEventDispatcher $dispatcher)
  {
    self::$dispatcher = $dispatcher;
  }
 
  /**
   * Returns the EventDispatcher
   *
   * @return sfEventDispatcher
   */
  static public function getEventDispatcher()
  {
    return self::$dispatcher;
  }
 
  /**
   * Returns if EventDispatcher is available
   *
   * @return boolean
   */
  static public function hasDispatcher()
  {
    return (self::getEventDispatcher() instanceOf sfEventDispatcher);
  }
}

Maintenant il faut spécifier à doctrine d’utiliser notre classe custom comme classe de référence des classes modèles, on utilise la méthode configureDoctrine() de sfProjectConfiguration. Aprés quoi on va avoir besoin de passer l’event dispatcher à notre classe. On va donc catcher l’événement « context.load_factories » qui est déclenché une fois que le contexte est initialisé pour aller initialiser la classe CustomDoctrineRecord.

Fichier : /config/ProjectConfiguration.class.php

<?php
 
require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();
 
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    parent::setup();
 
    $this->enablePlugins(array(
      'sfDoctrinePlugin',
    ));
 
    $this->dispatcher->connect('context.load_factories', array($this, 'listenToContextFactoriesLoaded'));
  }
 
  /**
   * Defines the base objects' classe
   */
  public function configureDoctrine(Doctrine_Manager $manager)
  {
    sfConfig::set('doctrine_model_builder_options', array(
      'baseClassName' => 'CustomDoctrineRecord'
    ));
  }
 
  /**
   * Objects can acces the EventDispatcher
   */
  public function listenToContextFactoriesLoaded(sfEvent $event)
  {
    CustomDoctrineRecord::setEventDispatcher($event->getSubject()->getEventDispatcher());
  }
}

Aprés ca, il nous reste à faire un petit

./symfony doctrine:build --all-classes

Et on peut vérifier l’inpact en ouvrant une classe de base, et on trouve bien que la classe hérite de notre classe CustomDoctrineRecord

Fichier : /lib/model/doctrine/base/BaseJobeetJob.class.php

<?php
...
abstract class BaseJobeetJob extends CustomDoctrineRecord
{
...

Il est donc maintenant possible de lever des événements depuis les classes du modèle. Pour exemple je vais lever un événement à l’insertion d’un nouveau Job. Cet événement pourra être utilisé par la suite pour faire ce que l’on veut, comme par exemple envoyer une email au modèrateur l’informant qu’un nouveau job a été saisi sur le site.

Fichier : /lib/model/doctrine/JobeetJob.class.php

<?php
 
class JobeetJob extends BaseJobeetJob
{
  /**
   * @param Doctrine_Event $event 
   */
  public function postInsert($event)
  {
    if (self::hasDispatcher())
    {
      if (self::getEventDispatcher()->hasListeners('JobeetJob.Inserted'))
      {
        self::getEventDispatcher()->notify(new sfEvent($this, 'JobeetJob.Inserted'));
      }
    }
  }
}

C’est un exemple tout simple, mais les possibilités sont immenses. Attention aux événements en cascades, ca peut très vite devenir un casse tête à debugger !

Dernier petit tips, pour pouvoir faire des tests de vos méthodes qui lèvent des événements : c’est à mis chemin entre les tests unitaires et les tests fonctionnels. J’ai voulu tester des enchainements de méthodes qui levaient des événements, et je voulais vérifier que les règles de gestion étaient respectées. Je suis donc parti du bootstap unitaire où j’ai initialisé un contexte que je passe à ma classe de base, de manière à ce que les événements soient diffusés.

Fichier : /test/bootstrap/unit_event.php

<?php
 
include(dirname(__FILE__).'/unit.php');
 
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);
$context = sfContext::createInstance($configuration);
CustomDoctrineRecord::setEventDispatcher($configuration->getEventDispatcher());

Quand on commence à utiliser les événements, il y a deux réactions possibles. Il y a les réfractaires qui disent « c’est null, ca sert a rien », soit on devient accro et on commence à en mettre de partout. Si vous faites parti du deuxième groupe, voici quelques conseils élémentaires mais important à respecter pour éviter que le debuggage se transforme en calvaire :

  • mettre des noms d’événements explicites ;
  • centraliser les écouteurs.

Bon codage à tous, et bonnes fêtes 😉

Show Comments (4)

Comments

  • NiKo

    Y’a quand même de drôle de problème avec la coloration syntaxique du code dans ce billet, dommage :/

    Sinon ravi d’avoir aidé/inspiré 🙂

    • Article Author
  • Samuel Breton

    Les couleurs sont maintenant ok. Je sais pas qui avait tout pété..

    • Article Author
  • Khepin

    Je suis retourné voir la doc vu qu’il me semblait qu’il y avait un moyen simple d’accéder à l’event dispatcher depuis absolument partout, et ce qu’ils disent c’est:

    $dispatcher = ProjectConfiguration::getActive()->getEventDispatcher();
    permet de récupérer l’event dispatcher depuis n’importe où dans l’application.

    Moi j’ai commencé à utiliser les évènements récemment, ma réaction serait plutôt, « mais pourquoi on n’a pas ça direct en standard dans le langage? ». Plutôt côté accros donc!

    • Article Author
  • Éric Rogé

    Avec ce système, vous mixez au sein du même projet le système d’événements de Doctrine et celui de Symfony.

    Je suis bien placé pour savoir que ça peut provoquer de mauvaises surprises. J’ai utilisé cette technique sur un projet qui lançait/écoutait un grand nombre d’événements (https://freelancer-app.fr) et je m’en suis mordu les doigts.

    Certains événements dépendaient du système de Doctrine et d’autres de celui de symfony. Je ne savais parfois plus qui lançait/écoutait quoi. Sur certains événements complexes, j’ai finis par me retrouver avec des problèmes liés à l’ordre d’appel des listeners et de synchronisation des objets en mémoire.

    J’ai presque fini à un refactoring en profondeur de mon projet, la quasi-totalité de mes événements sont gérés par doctrine.
    Je trouve que l’organisation du code s’en trouve bien mieux et qu’elle est au final plus logique: tous les événements qui modifient un composant du modèle sont gérés par les événements de Doctrine dont c’est quand même le boulot.

    Les quelques événements symfony restants sont très marginaux, comme par exemple des alertes emails qui ne modifient pas en retour le modèle.

    • Article Author

Recevez nos articles