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

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 😉

Partagez cet article