Programmation d'événements en PHP

Travaillant principalement en javascript (pour le plaisir et au boulot) et en java (uniquement au boulot - faut pas déconner !), je suis habitué à la programmation par gestion d'événements. Voici une solution que j'utilise en PHP pour retrouver un comportement comparable.

Introduction

La programmation par événement se base sur des objets qui émettent des événements et qui donnent la possibilité de se mettre à l'écoute de ces émissions pour effectuer des actions. Il s'agit d'un mode de programmation très courant avec des langages comme le javascript ou le java.

// object that emits events
var link = document.getElementById('myLink');
// event listener
var clickListener = function (e) {
    // do something when link is clicked.
};
// listen to click event
link.addEventListener('click', clickListener, false);
Exemple de l'écoute d'un événement click sur un lien en javascript.
public class MyClass implements ActionListener {
    public MyClass() {
        // attach listener to object that emits events
        objectWithAction.addActionListener(this);    
    }
    // event listener method
    public void actionPerformed(ActionEvent e) {
        // do something when action occured
    }
}
Exemple de l'écoute d'une action en java.

PHP : EventEmitter et CustomEvent

Il n'existe pas vraiment de structure permettant ce modèle de programmation en PHP, mais nous pouvons créer notre propre implémentation.

Classe EventEmitter

Il faut d'abord définir une API pour les objets qui seront capables d'émettre des événements. Elle sera portée par une classe EventEmitter qui devra être héritée :

<?php

namespace rnb\events;

/**
 *  Classe permettant de gérer l'émission d'événements.
 *
 *  La classe fournit des méthodes publiques pour attacher ou supprimer des
 *  écouteurs d'événements (\rnb\events\CustomEvent) ainsi 
 *  qu'une méthode interne pour émettre les dits événements.
 *
 *  @class \rnb\events\EventEmitter
 */
class EventEmitter 
{
    /**
     *  Liste des écouteurs, classés par type d'événements écoutés.
     *  @type array<string, array<callback>>
     */
    protected $listeners = [];
    
    public function connect($name, $listener) {
        return $this->addEventListener($name, $listener);
    }
    
    public function disconnect($type, $id) {
        return $this->removeEventListener($type, $id);
    }
    
    public function emit(CustomEvent $event) {
        return $this->dispatchEvent($event);
    }
    
    /**
     *  Ajouter un écouteur d'événement.
     *
     *  @param {string} $name Nom de l'événement
     *  @param {callable} $listener Ecouteur de l'événement.
     *  @return {int} identifiant de l'écouteur
     *  @throw {\InvalidArgumentException} Si l'écouteur n'est pas callable.
     */
    public function addEventListener($type, $listener) 
    {
        if (!is_callable($listener)) {
            throw new \InvalidArgumentException('listener is not callable');
        }
        if (!isset($this->listeners[$type])) {
            $this->listeners[$type] = [];
        }
        $this->listeners[$type][] = $listener;
        end($this->listeners[$type]);
        return key($this->listeners[$type]);
    }
    
    /**
     *  Supprimer un écouteur d'événement.
     *
     *  @param {string} $type Type de l'événement
     *  @param {callable|int} [$listener] Ecouteur ou identifiant d'écouteur à 
     *  supprimer. Si le paramètre n'est pas fourni, tous les écouteurs du type
     *  d'événement fournit seront supprimés.
     */
    public function removeEventListener($type, $listener) 
    {
        if (isset($this->listeners[$type])) {
            if (!$listener) {
                // Supprimer tous les écouteurs de l'événement.
                foreach($this->listeners[$type] as $id => $listener) {
                    unset($this->listeners[$type][$id]);
                }
            } elseif (is_int($listener)) {
                // Par indentifiant
                if (isset($this->listeners[$type][$listener])) {
                    unset($this->listeners[$type][$listener]);
                }
            } else {
                // Par écouteur
                $id = array_search($listener, $this->listeners[$type]);
                if ($id) {
                    unset($this->listeners[$type][$id]);
                }
            }
        }
    }
    
    /**
     *  Y-a-t-il des écouteurs attachés à un type d'événement ?
     *
     *  @param {string} $type Type de l'événement
     *  @return {boolean} True si des écouteurs sont attachés.
     */
    public function hasEventListeners($type) 
    {
        return isset($this->listeners[$type]) && count($this->listeners[$type]) > 0;
    }
    
    /**
     *  Récupérer la liste des écouteurs pour un type d'évenement.
     *
     *  @param {string} $type Le type d'événement.
     *  @return {array} La liste des écouteurs (vide s'il n'y en a aucun).
     */
    protected function getEventListeners($type) 
    {
        return isset($this->listeners[$type]) ? $this->listeners[$type] : [];
    }
    
    /**
     *  Eméttre un événement. La méthode parcours la liste des écouteurs pour
     *  ce type d'événement. Elle arrête cette lecture si l'événement est stoppé.
     *
     *  @param {\rnb\events\CustomEvent} $event L'événement à lancer
     *  @return {\rnb\events\CustomEvent} L'événement.
     */
    protected function dispatchEvent(CustomEvent $event) 
    {
        foreach ($this->getEventListeners($event->getType()) as $listener) {
            call_user_func($listener, $event);
            if ($event->isStopped()) {
                return $event;
            }
        }
        return $event;
    }
}
Classe Observable permettant à des objets d'émettre des événements.

Classe CustomEvent

Il nous faut ensuite un objet qui fera office d'événement :

<?php

namespace rnb\events;

/**
 *  Objet événement à émettre par les objet de type \rnb\events\EventEmitter.
 *  @class \rnb\events\CustomEvent
 */
class CustomEvent 
{
    /**
     *  Objet émetteur de l'événement.
     *  @type mixed
     */
    private $source;
    
    /**
     *  Données de l'événement.
     *  @type mixed
     */
    private $data;
    
    /**
     *  Type de l'événement.
     *  @type string
     */
    private $type = '';
    
    /**
     *  Valeur de retour de l'événement.
     *  @type mixed
     */
    private $returnValue = null;
    
    /**
     *  Flag indiquant que l'action par défaut associé à l'événement ne doit pas
     *  être exécuté.
     *  @type boolean
     */
    private $prevented = false;
    
    /**
     *  Flag indiquant si l'émission de l'événement a été arrêtée.
     *  @type boolean
     */
    private $stopped = false;
    
    /**
     *  Constructeur
     *  @param {string} $type Type de l'événement
     *  @param {mixed} $source L'objet émettant l'événement
     *  @param {mixed} [$data = null] Les paramètres de l'événement
     */
    public function __construct($type, &$source, $data = [], $returnValue = null) 
    {
        $this->type = $type;
        $this->source =& $source;
        $this->data =& $data;
        $this->returnValue = $returnValue;
    }
    
    /**
     *  Récupérer le type de l'événemennt.
     *  @return {string}
     */
    public function getType() 
    {
        return $this->type;
    }
    
    /**
     *  Récupérer l'objet qui émét l'évenement.
     *  @return {\rnb\events\EventEmitter}
     */
    public function getSource() 
    {
        return $this->source;
    }
    
    /**
     *  Récupérer la donnée associée à l'événement.
     *  @return {mixed}
     */
    public function getData() 
    {
        return $this->data;
    }
    
    /**
     *  Récupérer la valeur de retour de l'événement.
     *  @return {mixed}
     */
    public function getReturnValue() 
    {
        return $this->returnValue;
    }
    
    /**
     *  Définir la valeur de retour de l'événement.
     *  @param {mixed} $value La valeur à retourner par l'événement.
     */
    public function setReturnValue($value) 
    {
        $this->returnValue = $value;
    }
    
    /**
     *  Décider que l'éventuelle action associée à l'émission de l'événement 
     *  ne doit pas être effectuée.
     */
    public function prevent() 
    {
        $this->prevented = true;
    }
    
    /**
     *  L'éventuelle action associée est-elle bloquée ?
     *  @return {Boolean}
     */
    public function isPrevented()
    {
        return $this->prevented;
    }
    
    /**
     *  Arrêter l'émission de l'événement. Si cette méthode est utilisée, les autres
     *  écouteurs de l'événement ne seront pas appelés.
     */
    public function stop() 
    {
        $this->stopped = true;
    }
    /**
     *  L'émission de l'événement a-t-elle été arrêtée ?
     *  @return {Boolean}
     */
    public function isStopped()
    {
        return $this->stopped;
    }
}
Classe représentant un objet événement

Ceci est une manière de faire parmi d'autres. L'objet CustomEvent, en particulier, implémente une API connue en javascript sur les objets comparables et qui permet de stopper l'action qui devrait s'exécuter à la suite de l'émission de l'événement.

Utilisation

Illustrons l'utilisation des 2 objets, EventEmitter et CustomEvent, avec une classe qui émettra 2 événements, le premier avant une action lambda, le second après. Nous nous placerons ensuite à l'écoute de ces événements.

Objet émettant un événement

use \rnb\events\EventEmitter;
use \rnb\events\CustomEvent;

class MyObject extends EventEmitter {
    
    const EVENT_BEFORE_ACTION = 'before-action';
    const EVENT_AFTER_ACTION = 'after-action';
    
    private $myVar = 'foo';
    
    public function action() {
        // emit EVENT_BEFORE_ACTION event
        $event = new CustomEvent(self::EVENT_BEFORE_ACTION, $this);
        $this->dispatchEvent($event);
        
        // do action only if listener doesn't stop it
        if (!$event->prevented()) {
            $this->myVar = 'bar';
        }
        
        // emit EVENT_AFTER_ACTION event
        $event = new CustomEvent(self::EVENT_AFTER_ACTION, $this);
        $this->dispatchEvent($event);
    }
    
    public function getMyVar() {
        return $this->myVar;
    }
}
Implémentation de EventEmitter sur une classe MyObject.

A l'écoute des événements

$inst = new MyObject();
$inst->addEventListener(
    MyObject::EVENT_BEFORE_ACTION, 
    'onBeforeAction'
);
$inst->addEventListener(
    MyObject::EVENT_AFTER_ACTION, 
    'onAfterAction'
);
Se mettre à l'écoute des événement de l'objet MyObject.

Ecoute simple

Nous devons maintenant définir nos écouteurs. Contentons-sous simplement d'afficher la valeur de la propriété myVar :

// EVENT_BEFORE_ACTION listener
function onBeforeAction(CustomEvent $event) {
    echo $event->getSource()->getMyVar();
}
// EVENT_AFTER_ACTION listener
function onAfterAction(CustomEvent $event) {
    echo $event->getSource()->getMyVar();
}
Ecouteurs d'événements.
foo
bar
Résultats

Ecoute avec arrêt d'action

Annulons maintenant le comportement par défaut de l'objet lorsqu'il émet son premier événement et regardons le résultat :

// EVENT_BEFORE_ACTION listener
function onBeforeAction(CustomEvent $event) {
    echo $event->getSource()->getMyVar();
    $event->prevent();
}
// EVENT_AFTER_ACTION listener
function onAfterAction(CustomEvent $event) {
    echo $event->getSource()->getMyVar();
}
Ecouteurs d'événements.
foo
foo
Résultats