Hook de fonctions

ViPHP
xTG
ViPHP | 7331 Messages

16 oct. 2010, 16:12

Bonjour à toutes et à tous,

je cherche à mettre en place une classe gérant les hooks sur les fonctions filles (des classes étendant ma class hook).
Or je me retrouve confronté à un soucis. Comment réussir à déclencher un évènement lors de l'appel d'une fonction ?

J'ai passé un bon moment à parcourir php.net pour trouver solution à mon problème.
Je suis tombé donc sur la fonction magique __call qui permettent de gérer des hooks mais sur des éléments n'existant pas ou ne pouvant être retournés.

J'ai donc une solution temporaire mais qui ne me plaît guère, à savoir tout passer en protected ce qui aura comme conséquence de ne pouvoir être appelé depuis l'extérieur sans déclencher __call.
<?php
class hook
{
    private $tab_hook_nom = array('debut' => true, 'fin' => true);
    private $tab_hook_fonction = array();
    protected $nom_extends = "";
    
    public function __construct($nom)
    {
        $this->nom_extends = $nom;
    }
    
    public function __call($name, $arguments)
    {
        echo "Appel de la méthode '$name' " . implode(', ', $arguments). "<br />";
        if( method_exists($this,$name) )
        {
            echo "hook debut<br />";
            call_user_func_array(array($this->nom_extends, $name), $arguments);
            echo "hook fin<br />";
        }
    }
    
    protected function fonction2($nb1,$nb2,$nb3)
    {
        echo "fonction2 : $nb1, $nb2, $nb3<br/>";
    }
        
    protected function ajouter_hook($nom, $fonction)
    {
        if( !empty($this->tab_hook_nom[$nom]) )
        {
            $this->tab_hook_fonction[$nom][$fonction] = $fonction;
            return true;
        }
        else
            return false;
    }
    
    protected function supprimer_hook($nom, $fonction)
    {
        if( !empty($this->tab_hook_nom[$nom]) )
        {
            if( !empty($this->tab_hook_fonction[$nom][$fonction]) )
            {
                unset($this->tab_hook_fonction[$nom][$fonction]);
            }
        }
    }
}

class test extends hook
{
    protected $name = "test";
    
    public function __construct()
    {
        parent::__construct($this->name);
        echo "instanciation<br />";
    }
    
    protected function fonction1()
    {
        echo "fonction1<br />";
    }
}

$test = new test();
$test->fonction1(1,2,3);
$test->fonction2();

?>
Déroulement :
instanciation
Appel de la méthode 'fonction1' 1, 2, 3
hook debut
fonction1
hook fin
Appel de la méthode 'fonction2'
hook debut

Warning: Missing argument 1 for hook::fonction2() in C:\wamp\www\test.php on line 24

Warning: Missing argument 2 for hook::fonction2() in C:\wamp\www\test.php on line 24

Warning: Missing argument 3 for hook::fonction2() in C:\wamp\www\test.php on line 24

Notice: Undefined variable: nb1 in C:\wamp\www\test.php on line 26

Notice: Undefined variable: nb2 in C:\wamp\www\test.php on line 26

Notice: Undefined variable: nb3 in C:\wamp\www\test.php on line 26
fonction2 : , ,
hook fin
J'ai pour le moment toujours le soucis de passer l'array des paramètres à call_user_func_array(), la variable ne semble pas être reconnue comme un type array... Enfin c'est annexe pour le moment, je trouverai bien un moyen.

Que pensez-vous de cette solution ? Existe-t-il mieux ? Plus propre ?
Bien évidemment il aurait été plus simple de gérer les hooks directement dans les fonctions... Mais c'est une solution que je ne veux pas car je ne peux être sûr que l'utilisateur qui utilisera ma classe pensera à rajouter le code pour les hooks de sa fonction.

Mammouth du PHP | 19672 Messages

16 oct. 2010, 20:42

Regarde ton propre code : tu envoies des paramètres pour une méthode qui n'en attend pas, et aucun pour celle qui en attend trois... :-*
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
xTG
ViPHP | 7331 Messages

16 oct. 2010, 20:56

En effet... Je me sens bête du coup. 8-|

Concernant ma méthode pour faire des hooks qu'en pensez-vous ?

Mammouth du PHP | 19672 Messages

16 oct. 2010, 21:04

Disons que pour faire des appels vers des méthodes de l'objet lui-même, c'est un peu sans intérêt, mais le principe est là. J'utilise ça dans un package de gestion de formulaires maison et je n'ai jamais besoin d'instancier moi-même les différentes classes de champs de formulaire, tout passe par un hook du ma classe form.

Sinon, la manière dont tu fais l'appel devrait te générer des erreurs E_STRICT parce que ça effectue un appel statique : en réalité tu aurais dû voir ceci :

Code : Tout sélectionner

instanciation Appel de la méthode 'fonction1' hook debut Strict Standards: call_user_func_array() expects parameter 1 to be a valid callback, non-static method test::fonction1() should not be called statically, assuming $this from compatible context test in /var/www/tmp/hook.php on line 19 fonction1 hook fin Appel de la méthode 'fonction2' 1, 2, 3 hook debut Strict Standards: call_user_func_array() expects parameter 1 to be a valid callback, non-static method test::fonction2() should not be called statically, assuming $this from compatible context test in /var/www/tmp/hook.php on line 19 fonction2 : 1, 2, 3 hook fin
Pour résoudre ça, soit tu passes les méthodes en statique, soit tu remplace ceci :
call_user_func_array(array($this->nom_extends, $name), $arguments);
par cela :
call_user_func_array(array($this, $name), $arguments);
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
xTG
ViPHP | 7331 Messages

16 oct. 2010, 22:00

Merci, je n'ai en effet pas ce niveau d'erreur donc je ne la voyais pas.

Mon but est d'appeler des méthodes externes à la classe en fait.

Le main appelle la fonction1 de la classe test.
Le __call appelle une fonction externe à la classe puis appelle la fonction1.

Mammouth du PHP | 19672 Messages

16 oct. 2010, 22:29

Tu te heurteras à la même erreur E_STRICT si les méthodes appelées ne sont pas statiques. La solution passerait donc par une instanciation de la classe nécessaire. Tu peux donc envisager un système de switch pour identifier à quelle classe appartient la méthode requise. Et si les méthodes sont statiques, tu ne bénéficieras pas de l'héritage ni des propriétés de l'objet puisque ce dernier n'existera pas.

Très sommairement, voici à peu près comment j'utilise ça :
<?php
class hook
{
    public function __construct(){}
    public function __call($name, $arguments)
    {
        if(class_exists($name))
        {
            echo "Appel de la Classe '". $name ."' ". implode(', ', $arguments) ."<br />";
            return new $name($arguments);
        }
    }

    public static function fonction2($nb1,$nb2,$nb3)
    {
        return "fonction2 : ". $nb1 .", ". $nb2 .", ". $nb3 ."<br/>";
    }
}

class test extends hook
{
    protected $name = "test";

    public function __construct()
    {
        parent::__construct();
        echo "instanciation<br />";
    }

    public static function fonction1()
    {
        return "fonction1<br />";
    }
}

$hook = new hook();

$test = $hook->test();
echo("<pre>\n");
var_dump($test);
echo("</pre>\n");
echo($test->fonction1());
echo($test->fonction2(1,2,3));

?>
C'est très simplifié bien entendu. Différence principale, les méthodes appelées doivent être publiques. Je réserve en général les méthodes protégées aux classes abstraites qui ne sont jamais appelées que par les méthodes de classes elles-même. Là, je sais aussi exactement de quelle classe j'ai besoin, je pars d'un objet hook et je crée des instances en appelant la méthode ayant le nom de la classe voulue, ici « test » et j'accède alors au méthode de cette classe et de sa classe parente.
Exécute ça et observe le résultat.
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
xTG
ViPHP | 7331 Messages

17 oct. 2010, 00:02

Le soucis c'est qu'on s'éloigne totalement du sujet qui est le hook dans ce cas là.
Cela reste un appel dynamique qui se déporte un peu du standard mais qui y ressemble pour moi.
On n'exécute aucunement une action supplémentaire de ce fait. C'est même pire, c'est au programme principal de faire le déroulement du hook dans ton exemple.

Je trouve que la classe hook de ton exemple est une redondance simple et brute de la classe test. :/

Voilà où j'en suis arrivé pour ma part, mais cela ne règle en rien le problème des erreur E_STRICT dans le cas où on associe une fonction provenant d'une classe.
<?php
class hook
{
    /*
    * Liste des emplacements hook actifs
    * @type array(String => Boolean)
    */
    protected $tab_hook_nom = array('debut' => true, 'fin' => true);
    
    /*
    * Liste des fonctions hook
    * @type array(Mixed)
    */
    protected $tab_hook_fonction = array();
    
    /*
    * Appel de fonction
    */
    public function __call($name, $arguments)
    {
        // On vérifie que la méthode appelée existe
        if( method_exists($this,$name) )
        {
            // On prépare les arguments pour les fonctions hook
            $arguments_hook[0] = $name;
            $arguments_hook[1] = $arguments;
            
            // Appel des hooks de debut
            if( !empty($this->tab_hook_nom['debut']) && !empty($this->tab_hook_fonction['debut'][$name]) )
            {
                foreach($this->tab_hook_fonction['debut'][$name] as $fonction)
                    call_user_func_array($fonction, $arguments_hook);
            }
            
            // Appel de la fonction
            $retour = call_user_func_array(array($this, $name), $arguments);
            
            // Appel des hooks de fin
            if( !empty($this->tab_hook_nom['fin']) && !empty($this->tab_hook_fonction['fin'][$name]) )
            {
                foreach($this->tab_hook_fonction['fin'][$name] as $fonction)
                {
                    $arguments_hook[2] = $retour;
                    $retour = call_user_func_array($fonction, $arguments_hook);
                }
            }
            
            // Retour du résultat
            return $retour;
        }
    }
    
    /*
    * Méthode ajoutant un hook sur une fonction de la classe
    * @type String emplacement du hook
    * @type String fonction ciblée par le hook
    * @type String fonction appelée lors du hook
    * @return Boolean
    */
    protected function ajouter_hook($nom, $fonction, $fonction_hook)
    {
        if( !empty($this->tab_hook_nom[$nom]) )
        {
            $this->tab_hook_fonction[$nom][$fonction][] = $fonction_hook;
            return true;
        }
        else
            return false;
    }
    
    /*
    * Méthode supprimant un hook sur une fonction de la classe
    * @type String emplacement du hook
    * @type String fonction ciblée par le hook
    * @type String fonction appelée lors du hook
    */
    protected function supprimer_hook($nom, $fonction, $fonction_hook)
    {
        if( !empty($this->tab_hook_nom[$nom]) )
        {
            if( !empty($this->tab_hook_fonction[$nom][$fonction]) )
            {
                foreach($this->tab_hook_fonction[$nom][$fonction] as $key => $fct)
                {
                    if( $fct == $fonction_hook )
                    {
                        unset($this->tab_hook_fonction[$nom][$fonction][$key]);
                        break;
                    }
                }
            }
        }
    }
}

class test extends hook
{    
    protected function fonction1()
    {
        echo "exécution fonction1<br />";
    }
    protected function fonction2($nb1,$nb2,$nb3)
    {
        echo "exécution fonction2 : $nb1, $nb2, $nb3<br/>";
        return $nb1 + $nb2 + $nb3;
    }
}

$i = 0;

function mon_hook($fonction, $arguments)
{
    global $i;
    $i++;
}
function mon_hook2($fonction, $arguments, $retour)
{
    global $i;
    $i += 2;
    echo "$retour + 2(hook)";
    return $retour + 2;
}

$test = new test();
$test->ajouter_hook("debut","fonction1","mon_hook");
$test->ajouter_hook("fin","fonction2","mon_hook2");
$test->fonction1();
echo " = " . $test->fonction2(1,2,3) . "(main)<br />";
$test->supprimer_hook("fin","fonction1","mon_hook2");
$test->fonction1();
$test->fonction1();
$test->fonction1();
echo "i : " . $i . "<br />";
?>

Mammouth du PHP | 19672 Messages

17 oct. 2010, 09:36

:shock: Un détail a du m'échapper... peut-être serait-il opportun de s'entendre sur le but de la manœuvre et la signification donnée au « hook ».
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
xTG
ViPHP | 7331 Messages

17 oct. 2010, 10:13

Beh tout simplement celle-ci pour ma part de définition : http://fr.wikipedia.org/wiki/Hook_%28informatique%29 8-|
L'exemple de Tortoise SVN est très bien représentatif.
L'utilisateur va faire un commit, c'est l'action prévue entre toutes, mais on a placé deux hooks, l'un en début de fonction et l'autre en fin.
Ce qui fait que lors de l'appel de la fonction commit sera exécutée avant une fonction pre-commit et à la fin de la fonction sera exécutée une fonction post-commit.

Travaille-t-on donc avec le même terme ?

Mammouth du PHP | 19672 Messages

17 oct. 2010, 12:00

J'ai le sentiment qu'on dérive vers de l'abstraction totale : j'ai tendance pour ma part à rester sur un plan pratique.

Pour autant que je sache, on est en présence d'une technique qui aide à la mise en place de polymorphisme : on a une classe A et des classes qui l'étendent, Aa, Ab, Ac, etc.. À partir du hook, je dois pouvoir appeler les méthodes des classes Aa, Ab, Ac et autres selon des critères précis. On peut envisager deux aspects de l'ensemble : soit un ensemble de classes qui ont chacune un rôle spécifique et n'a rien à voir avec les autres et qui pourrait éventuellement être utilisée seule, mais le tout formant un groupe permettant certains traitements complexes, soit un ensemble de classes qui implémentent toutes la même interface, ont donc les mêmes méthodes et par conséquent implémentent différemment chacune de ces méthodes.

Partant de là, parlons-nous de la même chose, sinon quel est donc le but poursuivi ?
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
xTG
ViPHP | 7331 Messages

17 oct. 2010, 12:46

Le soucis avec le polymorphisme c'est qu'il faut que l'utilisateur le veuille. Tout comme ton code le présentait.

Le hook n'est aucunement cela. C'est même tout l'inverse.
Lorsqu'on clique sur un lien et qu'une popup de pub apparait en même temps. C'est une forme de hook. Tu déclenches deux traitements en n'en lançant qu'un seul.

Les hooks sont très utilisés dans les interfaces par exemple. En cet instant même tu en déclenches tout un tas.
Le fait de cliquer à un endroit précis de l'écran avec la souris déclenche un hook sur le programme actif.
Car ce n'est pas le programme qui gère la souris, c'est l'OS. Mais le programme a déposé un hook sur les fonctions de l'OS.
L'OS récupère les coordonnées du pointeur, le programme en post-traitement sait donc où le clic a eu lieu et peut agir en conséquence.

Mammouth du PHP | 19672 Messages

17 oct. 2010, 12:51

8-| Ok, et..?
On dérive complètement du sujet de départ, et tu ne réponds toujours pas à la question.
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

17 oct. 2010, 13:55

Et pourquoi ne pas passer par un système d'évènement ?

Tu aurais une classe évenement qui serait chargée de stocker les méthodes à exécuter (des callbacks), lors du déclenchement des exceptions, et qui serait chargée de les exécuter lors du déclenchement de l'évenement.

Un exemple d'utilisation serait le suivant :
class Foo
{
  public function doStuff()
  {
    EventClass::getInstance()->launch('event.name.pre_hook');
    // [...]
    EventClass::getInstance()->launch('event.name.post_hook');
  }
}
class Bar
{
  static public function foobar()
  {
    // [...]
  }

  static public function barfoo()
  {
    // [...]
  }
}

EventClass::getInstance()->connect('event.name.pre_hook', array('Bar', 'foobar'));
EventClass::getInstance()->connect('event.name.post_hook', array('Bar', 'barfoo'));
Comme ça, de cette manière, tu gères la gestion des évènements de manière externe, tu utilises des hooks tels que tu l'entends, et tu n'es pas obligé d'hériter d'une classe pour gérer tes hooks.

Pour ne rien te cacher, ce que je viens de t'expliquer est basé sur le système d'évènements de Symfony (sfEventDispatcher, sfEvent), et tu pourrais te baser sur ce système, ou alors partir sur ce que je te présente ;)
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer

ViPHP
xTG
ViPHP | 7331 Messages

17 oct. 2010, 14:17

8-| Ok, et..?
On dérive complètement du sujet de départ, et tu ne réponds toujours pas à la question.
Justement c'est le sujet de départ que je tente malgré moi de t'expliquer. C'est ton interprétation de mon problème qui dérive totalement en fait. :?
Enfin dans tous les cas on ne se comprends pas l'un l'autre. ^^
Pour ne rien te cacher, ce que je viens de t'expliquer est basé sur le système d'évènements de Symfony (sfEventDispatcher, sfEvent), et tu pourrais te baser sur ce système, ou alors partir sur ce que je te présente
C'est bien plus dans ce que je recherche. Je dois avouer que je n'ai pas compris l'ordonnancement de ton exemple mais je vais jeter un oeil dès que j'ai le temps sur la documentation de ces classes de Symphony.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

17 oct. 2010, 16:56

OK, je vais essayer d'être plus clair, même si le sujet, et ma faculté à me perdre de temps en temps dans mes explications peut être un frein ;)

Dans un 1er temps, il te faut un classe pour gérer les évènements.
Je te donne un draft non testé, non complet, juste pour planter le décor :
<?php

class EventManager
{
	static protected $instance = null;
	
	/**
	 * Liste des événements paramétrés
	 * 
	 * @var array
	 */
	protected $a_event = array();
	
	
	/**
	 * Constructeur non public pour le singleton
	 */
	protected function __construct() {}
	
	/**
	 * Singleton
	 */
	public function getInstance()
	{
		if( is_null(static::$instance) )
		{
			static::$instance = new self();
		}
		
		return static::$instance;
	}
	
	/**
	 * Ajout d'un callback à un événement
	 * 
	 * @param $event_name - Nom de l'événement à ajouter
	 * @param $callback - Méthode à exécuter
	 * @param $a_params - Paramètres à passer à la méthode
	 */
	public function connect($event_name, $callback, $a_params=array())
	{
		// Ajout du callback pour l'événement
		$this->a_event[$event_name][] = array('callback' => $callback, 'parameters' => $a_params);
	}
	
	/**
	 * Déclenchement de l'événement
	 * 
	 * @param $event_name
	 */
	public function launch($event_name)
	{
		if( array_key_exists($event_name, $this->a_event) )
		{
			// Pour chaque méthode ajouter à l'évènement, exécution de la méthode
			foreach( $this->a_event[$event_name] as $a_callback )
			{
				call_user_func($a_callback['callback'], $a_callback['parameters']);
			}
		}
	}
}
Ensuite, tu veux définir une classe générique, prenons par exemple une classe qui envoi un email :
class MailManager
{
	// [...]
	
	public function send($from, $to, $message)
	{
		// Mise en place d'un pre hook (execution de code avant l'exécution de la méthode)
		EventManager::getInstance()->launch('EmailManager.send.pre_hook');
		
		// Le code d'envoi d'email
		
		// Mise en place d'un post hook (execution de code après l'exécution de la méthode)
		EventManager::getInstance()->launch('EmailManager.send.post_hook');
	}
	
	// [...]
}
Dans l'état actuel des choses, ta classe d'envoi de mail fonctionne, et les hooks sont en place, rien de plante dans l'état, même si les hooks ne sont actuellement pas utilisés.

Maintenant, dans un de tes projets, il faut que tu log tout envoi de mail.
Du coup, tu te fais une petite classe de log :
<?php

class LogManager
{
	// [...]
	
	static public function doLog($message)
	{
		// doLog
	}
	
	// [...]
}
Maintenant, pour mettre en place les hooks, il ne te reste plus qu'à les configurer, tôt dans l'exécution de ton code (généralement dans le fichier d'initialisation de ton application) :
<?php

// Configuration des hooks
EventManager::getInstance()->connect('EmailManager.send.pre_hook', array('LogManager', 'doLog'), array('message' => 'Envoi d\'email : START'));
EventManager::getInstance()->connect('EmailManager.send.post_hook', array('LogManager', 'doLog'), array('message' => 'Envoi d\'email : OK'));
Et, à la condition que les hooks soient configurés, toute exécution de ta classe de mail entrainera un log

Bon, comme je le disais plus haut, c'est un simple draft, qu'il faut corriger (paramètres des callbacks), améliorer et adapter à ta sauce.
Mais je pense avoir planter les fondations et expliqué mon idée.

De toute manière, je renouvelle ce que je te disais plus haut, regarde du côté de l'EventDispatcher de Symfony, qui est déjà codé et fonctionnel ;)
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer