Programmation événementielle en PHP ⠠⠵

Le paradigme de programmation événementielle semble peut connu, en tout cas peut utilisé par les développeurs PHP, peut être parce que la gestion des signaux n'est pas incluse dans le langage, contrairement au C# ou au Javascript.

Petit manuel explicatif...

Le principe de la programmation événementielle est simple : une entité (dans notre cas, ce sera un objet) à la possibilité d'envoyer un événement, généralement suite à une action réalisée, et le développeur est en mesure d'intercepter cette événement grâce à des fonctions de rappel (callback en anglais) pour y adjoindre un traitement supplémentaire. Ce dernier mot est important, il ne faut pas voir cela comme une solution pour court-circuiter un traitement mais comme une manière d'effectuer des actions supplémentaires.

Exemple concret : vous souhaitez écrire dans un fichier de log les connexions ayant échouées, il va vous suffit d'intercepter un signal login-fail envoyé par l'objet gérant l'identification pour écrire une ligne dans un fichier. Par contre il sera difficile de faire en sorte que système de d'identification utilise un autre mécanisme pour authentifier les utilisateurs.

Au niveau des inconvénients majeurs de la programmation événementielle : il faut que les signaux soient prévus par le développeur du cœur et il n'est généralement pas possible d'ordonner les appels aux fonctions de rappel.

En contre partie, on y gagne en souplesse, en rapidité et couplé à la POO, il est possible d'ajouter des traitements simplement, sans utiliser l'héritage, et de conserver ce dernier pour modifier en profondeur un système.

Voici un exemple d'implémentation des signaux en PHP dans le cadre de mon framework d'expérimentation NanoMVC :

<?php
namespace NanoMvc;
 
/**
 * Classe de gestion des signaux.
 *
 * Copyright (C) 2011 Nicolas Joseph
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    NanoMvc
 * @author     Nicolas Joseph <gege2061@homecomputing.fr>
 * @copyright  2011 Nicolas Joseph
 * @license    http://www.opensource.org/licenses/agpl-3.0.html AGPL v3
 * @filesource
 */
 
class Signal
{
  /**
   * @var array
   */
  protected static $_callback_list;
 
  private static function _get_signal_hash ($object, $signal_name)
  {
    return sha1 (spl_object_hash ($object) .'-'. $signal_name);
  }
 
  /**
   * Créer un nouveau signal l'object $object.
   *
   * @param StdClass $object Object auquelle ajouter le signal
   * @param string $name Nom du signal
   */
  public static function create ($object, $name)
  {
    $hash = self::_get_signal_hash ($object, $name);
    self::$_callback_list[$hash] = array ();
  }
 
  /**
   * Supprime signal pour l'object $object.
   *
   * @param StdClass $object Object auquelle supprimer le signal
   * @param string $name Nom du signal
   */
  public static function delete ($object, $name)
  {
    if (self::has ($object, $name))
    {
      $hash = self::_get_signal_hash ($object, $name);
      unset (self::$_callback_list[$hash]);
    }
    else
    {
      $msg = sprintf ('Signal "%s" inconnu pour l\'instance %p (%s)',
        $name, $object, get_class ($object));
      throw new \Exception ($msg);
    }
  }
 
  /**
   * Vérifie si l'objet $object possède le signal $name
   *
   * @param StdClass $object Object auquelle supprimer le signal
   * @param string $name Nom du signal
   *
   * @return bool
   */
  public static function has ($object, $name)
  {
    $hash = self::_get_signal_hash ($object, $name);
    return isset (self::$_callback_list[$hash]);
  }
 
  /**
   * Connecte une fonction de rappel au signal $name de l'objet $object.
   *
   * @param StdClass $object Object à connecter
   * @param string $name Nom du signal
   * @param function $callback Fonction de rappel
   *
   * @return int L'identifiant unique de la connexion. Voir Signal::disconnect
   */
  public static function connect ($object, $name, $callback)
  {
    $id = -1;
 
    if (self::has ($object, $name))
    {
      $id = uniqid ();
      $hash = self::_get_signal_hash ($object, $name);
      self::$_callback_list[$hash][$id] = $callback;
    }
    else
    {
      $msg = sprintf ('Signal "%s" inconnu pour l\'instance %p (%s)',
        $name, $object, get_class ($object));
      throw new \Exception ($msg);
    }
    return $id;
  }
 
  /**
   * Déconnecte le signal portant l'identifiant $id de l'object $object.
   *
   * @param StdClass $object Objet connecté au signal
   * @param string $name Nom du signal
   * @param int L'indentifiant du signal
   */
  public static function disconnect ($object, $name, $id)
  {
    if (self::has ($object, $name))
    {
      $hash = self::_get_signal_hash ($object, $name);
      unset (self::$_callback_list[$hash][$id]);
    }
    else
    {
      $msg = sprintf ('Signal "%s" inconnu pour l\'instance %p (%s)',
        $name, $object, get_class ($object));
      throw new \Exception ($msg);
    }
  }
 
  /**
   * Émet le signal $name sur l'object $object.
   *
   * @param StdClass $object Objet envoyant le signal
   * @param string $name Le nom du signal à envoyé
   * @param mixed ... Liste variable d'arguments passée à la fonction de rappel
   */
  public static function emit ($object, $name/*, ...*/)
  {
    if (self::has ($object, $name))
    {
      $hash = self::_get_signal_hash ($object, $name);
      $callbacks = self::$_callback_list[$hash];
      if (!empty ($callbacks))
      {
        $args = func_get_args ();
        unset ($args[1]);
 
        foreach ($callbacks as $callback)
        {
          call_user_func_array ($callback, $args);
        }
      }
    }
    else
    {
      $msg = sprintf ('Signal "%s" inconnu pour l\'instance %p (%s)',
        $name, $object, get_class ($object));
      throw new \Exception ($msg);
    }
  }
}
 
 

Voici un exemple simple pour comprendre le déroulement des événements :

<?php
 
require_once 'signal.php';
 
class Sender
{
  public function __construct()
  {
    \NanoMvc\Signal::create($this, 'init');
  }
 
  public function init()
  {
    \NanoMvc\Signal::emit($this, 'init');
  }
}
 
$s = new Sender();
 
\NanoMvc\Signal::connect($s, 'init', function() {
  print 'Sender::init';
});
 
$s->init();
 

Étrangement sur Wikipédia cette technique est classée comme design pattern, je ne pense pas que ce soit le cas, par contre elle peut être utilisée pour le patron observateur qui, étrangement, est directement disponible en PHP grâce aux classes SplObserver et SplSubject ?! Allez savoir pourquoi...

sanpi, le 2011-06-17T14:34:34+02:00
Faire un don