Lab : PHP Une autre façon de programmer : EventDispatchers & EventListeners

Principes de développement

Le principe et son avantage

Le principe est simple : on va avoir un objet qui envoie des événements et d'autres objets qui vont écouter et réagir en fonction des événements qu'ils vont recevoir. J'ai vu dans de nombreux scripts php 'publiques', des fois de grosses productions, mettre dans leur doc : 'pour gérer la réponse étendre l'objet et créer une méthode qui s'appelle onStatus celle-ci sera appelée automatiquement par l'objet parent. Et en fait quand on regardait dans le code il y avait juste un appel dans l'objet à étendre this.onStatus($string) et donc l'objet fourni ne pouvait fonctionner sans être étendu et devait être étendu autant de fois que l'on voulait gérer l'événement avec tel ou tel objet. De la folie... folie soignable grâce au principe d'EventDispather et d'EventListener.


L'objet qui va émettre va avoir une méthode addEventListener qui va permettre de 'brancher' les objets qui doivent réagir à tel ou tel événement. Quand un événement survient, le dispatcher va alors utiliser sa méthode de dispatching pour informer tous ses listeners de cet Event.

De cette manière plus besoin d'étendre, il suffit juste de rajouter l'objet qui va gérer l'event comme listener de l'objet qui va provoquer l'event.

Pourquoi ?

L'explication du principe répond déjà à cette question mais une autre raison était que : développant dans plusieurs langages, j'essaie toujours d'uniformiser ma technique de programmation afin de ne pas être trop 'perturbé' en passant d'un langage à un autre. Au début même en flash je faisais la grimace lorsque je voyait les eventListeners puis après avoir cherché à les comprendre et les avoir utilisés dans la plupart de mes applications flash (et aujourd'hui en AS3 dans toutes mes applications), je les ai transposées en flashmedia et en javascript (ce qui n'était pas trop compliqué vu que ces deux environnements ont la même base que le 'client side actionscript', pour PHP c'est une autre histoire...

La grosse différence avec Actionscript et PHP c'est qu'en php les événements sont linéaires et arrivent un par un (ben oui pas encore de multithreading chez php... ). Il va falloir donc planter tout le décors, ajouter les listeners et seulement après tout ça lancer toutes les actions PHP.

L'objet EventDispatcher

Le code

Donc deux objets à faire : le dispatcher qui va envoyer le message et l'objet qui contient l'événement et son contenu :

<?php
/**
 * Un event dispatcher un peu comme en ActionScript 3
 *
 * @category   Events
 * @package    be.myconcept.phplab
 * @subpackage be.myconcept.phplab.events
 * @author     Yannick Molitor - http://myconcept.be
 * @version    1.0
 * @link       http://www.myconcept.be
 * @Modified   may 06
 * @since      1.1
 * @copyright  Copyright © 2006 - Yannick Molitor 
 * @license    MIT Licence > http://fr.wikipedia.org/wiki/Licence_X11 
 *  
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE. 
 *  
 **/
class EventDispatcher {
    private 
$listeners=array();
    public function 
addEventListener($type,$listener){
        
$this->listeners[$type][]=$listener;
    }
    public function 
removeEventListener($type,$listener){
                 
$list_cpt=count($this->listeners[$type]);
                 for(
$i=0;$i<$list_cpt;$i++){
                         if(
$listener===$this->listeners[$type][$i]){
                                unset(
$this->listeners[$type][$i]);
                        }
                 }
    }
    public function 
dispatchEvent($event){
        if(isset(
$this->listeners[$event->type])){
            foreach(
$this->listeners[$event->type] as $listener){
                 
call_user_func_array($listener,array($event));
            }
        }
    }
}
/**
 * Event de base à étendre et utiliser avec un objet EventDispatcher
 *
 * @category   Events
 * @package    be.myconcept.phplab
 * @subpackage be.myconcept.phplab.events
 * @author     Yannick Molitor - http://myconcept.be
 * @version    1.0
 * @link       http://www.myconcept.be
 * @Modified   may 06
 * @since      1.0
 * @copyright  Copyright © 2006 - Yannick Molitor 
 * @license    MIT Licence > http://fr.wikipedia.org/wiki/Licence_X11 
 **/
abstract class Event{
    public 
$datas;
    public 
$type;
    public function 
__construct($type,$datas,$target=false){
        
$this->type=$type;
        
$this->datas=$datas;
        
$this->_target=$target;
    }
}
? >

Demos

Cas de figure 1 : la gestion des erreurs de ce site

Voici le début du distributeur de ce site. Les chargements d'objets sont automatiques (cf article à ce sujet ) et tous les catch d'exceptions sont gérés et renvoyés si nécessaire en amont pour dispatcher l'event. Aucune action n'est entreprise avant la ligne $tc->output($pc->process()).

Dans ce cas il n'y a qu'un seul type d'énénement (ErrorEvent) mais il y aurait pu en avoir d'autres.

<?php
Registry
::init();
Config::setRegistry();
MyconceptV5Adapter::transformRegistry($_GET);
$tools          = new Library();
$bench          = new BenchMark();
$log            = new Logger();
$pc             PageController::factory();
$tc             TemplateController::factory();
$cc             CacheController::factory();
//Error Handler
$errorHandler   = new ErrorEventHandler();
//PHP Special actions
set_error_handler(array($errorHandler,"onPHPError"));
register_shutdown_function(array($pc,"onShutDown"));
//Register Event Listeners
//Monitor PageController Events
$pc->addEventListener(ErrorEvent::ERROR   , array($errorHandler,"onError"));
$pc->addEventListener(ErrorEvent::WARNING , array($errorHandler,"onWarning"));
$pc->addEventListener(ErrorEvent::NOTICE  , array($errorHandler,"onNotice"));
//Monitor TemplateController Events
$tc->addEventListener(ErrorEvent::ERROR   , array($errorHandler,"onError"));
$tc->addEventListener(ErrorEvent::WARNING , array($errorHandler,"onWarning"));
$tc->addEventListener(ErrorEvent::NOTICE  , array($errorHandler,"onNotice"));
//Monitor CacheController Events
$cc->addEventListener(ErrorEvent::ERROR   , array($errorHandler,"onError"));
$cc->addEventListener(ErrorEvent::WARNING , array($errorHandler,"onWarning"));
$tc->output($pc->process());
...

Cas de figure 2 : Serveur de Socket en PHP

Exemple de l'utilisation des EventListeners dans le cas de la gestion d'événements d'un serveur de socket écrit en PHP :

<?php
$svr
=new SocketServer("chatserver.myconcept.local");
//Si serveur prêt on installe les listeners et on démarre le service
if(isset($svr)){
    function 
onAppStart($e){
        echo 
"Application is running";
    }
    function 
onAppStop($e){
        echo 
"Application is stopped";
    }
    function 
onConnect($e){
       echo 
"A new client is online ".$e->datas;
       
$svr->broadCast($e->_target,"@SERVER : ".$e->_target->link_infos["host"]." is in the place... Make some noise !");
       
$e->_target->send(str_replace("%USER%",$e->_target->link_infos["host"],$svr->wMsg));
    }
    function 
onDisconnect($e){
        echo 
"A client is disconnected (#".$e->datas["id"].")";
        
$svr->broadCast(false,"@SERVER : ".$e->datas["link_infos"]["host"]." is gone !".">",true);
    }
    function 
onCall($e){
        echo 
"New Message";
        
$svr->broadCast($e->_target,$e->_target->link_infos["host"]." > ".$e->datas.">",true);
        
$e->_target->send(">");
        echo 
"Message broadcasted";
    }
    
$svr->addEventListener(SocketServerEvent::APP_START ,"onAppStart");
    
$svr->addEventListener(SocketServerEvent::APP_STOP  ,"onAppStop");
    
$svr->addEventListener(SocketServerEvent::CONNECT   ,"onConnect");
    
$svr->addEventListener(SocketServerEvent::DISCONNECT,"onDisconnect");
    
$svr->addEventListener(SocketServerEvent::CALL      ,"onCall");
    echo 
"Server Ready";
    
$message=array();
    
$message[]="#############################################";
    
$message[]=">>";
    
$message[]=">>              MY TELNET CHAT SERVER";
    
$message[]=">>";
    
$message[]="#############################################";
    
$message[]="";
    
$message[]="Welcome %USER%";
    
$message[]=">";
    
$svr->wMsg=implode(PHP_EOL,$message);
    try{
        
$svr->start();
    }
    catch(
Exception $e){
        echo 
"Erreur lors du lancement du service";
    }
}
- MY Concept FlashPlayer Loader for actionscript demo's and movie/animation player-