[php5] prototype: classe simulant l'overloading des fonctions ou méthodes

Description

Le problème

Dans certains languages, il est possible de définir plusieurs fonctions portant le même nom (on appelle alors cela des surcharges), qui sont alors différenciées par le nombre et le type de leurs paramètres. La liste de ces paramètres s'appelle la signature de la fonction.

PHP ne permettant pas (dans sa version actuelle) de définir plusieurs fonctions portant le même nom, il est normalement nécessaire de passer par toute une série de tests et de conditions pour définir le traitement à effectuer selon le nombre et le type de paramètres qu'elle reçoit.

Le fait que PHP soit un language sans typage fort implique une autre de ses limitations majeures: il ne gère pas pleinement le Type Hinting. En effet, pour le moment, les seuls types que l'on peut imposer pour des paramètres de fonctions ou de méthodes sont les tableaux et les objets, en spécifiant le nom de la classe attendue.

Comme vous vous en doutez, le nom «Prototype» n'a pas été choisi au hasard: en php, le prototype d'une fonction est sa définition. Comme expliqué plus haut, php a quelques lacunes au niveau des prototypes, obligeant systématiquement opérer à toute une série de tests et véréfication pour être sur du type des paramètres

C'est dans l'optique de simplifier ces tâches que la classe Prototype à été créée. Puisqu'il n'était pas possible de réécrire core de PHP, pour arriver au résultat voulu il a fallu passer par une astuce. En tout bon programmeur que vous êtes, vous commentez votre code je suppose? (Et quand bien même ce serait pas le cas, cela vous donnera une bonne raison de vous y mettre)

Prototype utilisera donc ces commentaires (en plus du Type Hinting habituel) pour déterminer quels sont les types attendus pour les paramètres. Cela demande un peu de rigueur au moment de la rédaction de vos commentaires, bien que ce type de syntaxe tente à se standardiser.

Source / Exemple :


<?php
/**

  • Fichier de la classe Prototype
*
  • @author Blétard Pascal <paddelman@hotmail.com>
  • @package Prototype
  • @version 0.2 beta ( 20 oct 2007 )
*
  • @todo
  • - écrire et completer la documentation et les exemples
  • - checker si paramètres passés par référence, et si fonction retournant une référence
*
  • @bug
  • - overloadArgs ( ) : Si l'argument qu'on lui passe est un tableau
  • (et non un tableau d'arguments), la fonction ne fait pas de différence
  • /
/**
  • Classe émulant les principes de surcharge et Type Hinting.
*
  • @author Blétard Pascal <paddelman@hotmail.com>
  • @package Prototype
  • @version 0.5 beta ( 26 oct 2007 )
  • /
class Prototype { /**#@+
  • @access private
  • /
/**
  • Utiliser DocComment?; Défaut: true
  • @var Bool
  • /
private $_use_doc_typehint; /**
  • Voir useStrictArgsNum() pour info; Défaut: false
  • @var Object
  • /
private $_use_strict_args_num; /**
  • Tester le pattern sur les fonctions internes?; Défaut: false
  • @var Object
  • /
private $_use_internal_match; /**
  • Champs d'action; Défaut: $this
  • @var Object|null
  • /
private $_scope; /**
  • Masque à utiliser lors d'un appel à la methode __call
  • @var string|null
  • /
private $_patterns; /**
  • Regex pour la recherche de types alternatifs dans le DocComment
    • /
private static $TYPE_SEARCH = array( '/\|/','/ integer /','/ boolean /','/ (double|real) /', '/ number /','/ callback /','/\s/' ); /**
  • Remplacement pour le regex
    • /
private static $TYPE_REPLACE = array(' | ','int','bool','float','numeric','callable'); /**
  • types associé à une fonction is_... (is_int(), is_strng(), ...)
    • /
private static $TYPE_ACCEPT = array( 'int','string','bool', 'float','numeric','scalar', 'callable','resource','array', 'object','null' ); /**#@-*/ function __construct( $scope = false, $doc_typehint = true, $strict_args_num = false, $match_in_internal = false ) { if ( !is_object($scope) && $scope !== null ) { $scope = $this; } $this->setScope($scope); $this->useDocTypehint($doc_typehint); $this->useStrictArgsNum($strict_args_num); $this->useInternalMatch($match_in_internal); } /**
  • Défini le champs d'action à utiliser.
*
  • Si vous ne spécifiez pas de $scope ou qu'il a la valeur null, le champs d'action sera
  • alors global.
  • {@example ../demo/ScopeExample.php}
  • <b>Note:</b> un appel implicite est fais à setScope() lors de l'instanciation
  • de Prototype, avec pour valeur l'object courrant.
  • @param Object|null $scope L'objet sur lequel appeler les méthodes de surcharge
  • ou null pour utiliser des fonctions.
  • @see getScope()
  • /
final public function setScope ( $scope = null ) { if ( !is_object($scope) ) { $scope = null; } $this->_scope = $scope; } /**
  • Retourne le champs d'action utilisé.
  • @return Object|null L'objet sur lequel les methodes de surcharge agissent,
  • ou null si le champs d'action est global.
  • @see setScope()
  • /
final public function getScope ( ) { return $this->_scope; } /**
  • Défini les surcharges à utiliser lors d'un appel à la fonction fictive $name
  • Le paramètre $pattern peut avoir plusieurs formes:
  • - Un tableau de noms de fonctions représentant les surcharges à tester.
  • La recherche s'effectuera dans l'ordre où elles apparaisse dans le tableau.
  • - Un masque Regex correspondant aux surcharges, la chaîne "___FUNCTION___"
  • placée dans le masque sera remplacée par le nom de la fonction fictive.
  • La recherche s'effectuera dans l'ordre où les surcharges ont été définies dans
  • le code php
  • - NULL, pour ne pas surcharger la fonction $name
*
  • @param String $name le nom de la fonction fictive que vous désirez
  • surcharger
  • @param Array|String|null $pattern le masque de recherche pour les fonctions de surcharge
*
  • @see getDefaultPattern(), __call()
  • /
final public function setOverload ( $name, $pattern = null ) { if ( !is_string($pattern) && !is_array($pattern) ) { $pattern = null; } $this->_patterns[$name] = $pattern; } /**
  • Retourne la ou les surcharges préalablement définies avec {@link setOverload()}
  • @return Array|String|null
  • @see setDefaultPattern(), __call()
  • /
final public function getOverload( $name = null ) { if ( null === $name ) { return $this->_patterns; } elseif ( isset($this->_patterns[$name]) ) { return $this->_patterns[$name]; } else { return null; } } /**
  • Défini si les méthodes de surcharge peuvent regarder dans le <i>DocComment</i>
  • précédent la fonction pour savoir quel est le type attendu de chaque paramètre.
*
  • {@example ../demo/DocTypehintExample.php}
  • @param bool|null $useDoc
  • @return bool L'ancienne valeur
  • /
final public function useDocTypehint ( $use_doc_typehint = null ) { $return = $this->_use_doc_typehint; if ( $use_doc_typehint !== null ) { $this->_use_doc_typehint = $use_doc_typehint; } return $return; } /**
  • Défini s'il ne faut prendre en compte que les fonctions/méthodes qui ont le même
  • nombre de paramètres (facultatifs compris) que le nombre d'arguments passés aux
  • méthodes {@link overload()}, {@link overloadArg()} et {@link overloadArgs()}
  • {@link overloadMatch()} et {@link overloadMatchArgs()}.
  • {@example ../demo/StrictArgsNumExample.php}
  • @param bool|null $set
  • @return bool
  • /
final public function useStrictArgsNum ( $use_strict_arg = null ) { $return = $this->_use_strict_args_num; if ( $use_strict_arg !== null ) { $this->_use_strict_args_num = $use_strict_arg; } return $return; } /**
  • Défini s'il faut prendre en compte les fonctions internes à php lors d'un appel aux
  • méthodes {@link overloadMatch()}, {@link overloadMatchArg()} et {@link overloadMatchArgs()}
  • {@link overloadMatch()} et {@link overloadMatchArgs()}.
  • Note: Les fonctions utilisateur passent avant les fonctions internes.
  • @param bool|null $set
  • @return bool
  • /
final public function useInternalMatch( $use_internal_match = null ) { $return = $this->_use_internal_match; if ( $use_internal_match !== null ) { $this->_use_internal_match = $use_internal_match; } return $return; } /**
  • Simule la surcharge d'une fonction ou d'une méthode.
*
  • Fonctionne de manière similaire à {@link overloadArgs()}
  • ormis le fait qu'il n'y ait pas besoin de spécifier d'arguments,
  • ceux-ci étant récupérés depuis le contexte où est appelé overload.
*
  • {@example ../demo/syntaxExemple01.php}
*
  • C'est pour cette raison, qu'il est impératif que cette méthode
  • soit appelée depuis une fonction ou une méthode.
  • Dans le cas contraire, une exception sera lancée.
  • @param mixed $funcs Soit un tableau de fonctions à tester.
  • Soit une liste de fonctions à tester,
  • séparées par des virgules
  • @return mixed
  • @thrown
  • /
final public function overload ( $funcs ) { try { $params = debug_backtrace(); if ( !isset($params[1]) ) { throw new Exception('Unable to get arguments: Invalid Scope ?', E_USER_WARNING); } if ( !is_array($funcs) ) { $func = func_get_args(); } return $this->overLoadArgs($funcs, $params[1]['args']); } /**
  • S'il y a une exception, on la relaye
  • /
catch (Exception $e) { throw $e; } } /**
  • Simule la surcharge d'une fonction ou d'une méthode en spécifiant l'argument
  • que l'on doit lui passer.
  • @param mixed $funcs Soit un tableau de fonctions à tester.
  • Soit une liste de fonctions à tester,
  • séparées par des virgules
  • @param mixed $param Un argument à passer à la fonction de surcharge
  • @return mixed
  • @thrown
  • /
final public function overloadArg ( $funcs, $param ) { try { if ( !is_array($funcs) ) { $funcs = func_get_args(); $param = array_pop($funcs); } return $this->overloadArgs($funcs, array($param)); } /**
  • S'il y a une exception, on la relaye
  • /
catch ( Exception $e ) { throw $e; } } /**
  • Simule la surcharge d'une fonction ou d'une méthode en spécifiant les arguments
  • que l'on doit lui passer.
  • La surcharge qui sera appelée sera la première de la liste dont la <i>signature}</i>
  • correspond aux paramètres $params.
*
  • ATTENTION: Si vous ne passez qu'un seul paramètre $params et que celui-ci est un tableau,
  • le comportement du script risque d'être imprévisible. Ainsi, si vous n'avez qu'un paramètre
  • à donner, passez plutôt par {@link overloadArg()}.
  • @param mixed $funcs Soit un tableau de fonctions à tester.
  • Soit une liste de fonctions à tester,
  • séparées par des virgules
  • @param array $params Un tableau d'arguments
  • @return mixed
  • @thrown
  • /
final public function overloadArgs ( $funcs, $params ) { if ( !is_array($funcs) ) { $funcs = func_get_args(); $params = array_pop($funcs); } if ( !is_array($params) ) { $params = array($params); } $num_args = count($params); $scope = $this->getScope(); foreach ( $funcs as $func_string ) { try { if ( $scope ) { $func = new ReflectionMethod($scope, $func_string); } else { $func = new ReflectionFunction($func_string); } if ( $func->getNumberOfRequiredParameters() > $num_args ) { throw new ReflectionException("Wrong parameter count for '$func_string()'"); } if ( $this->useStrictArgsNum() && $func->getNumberOfParameters() != $num_args ) { throw new ReflectionException("Number of params not Match for '$func_string()' (strict mode)"); } foreach ( $func->getParameters() as $i => $param ) { /**
  • Si le paramètre est optionnel et null, on passe au suivant
  • /
$isnull = ($param->isOptional() && $params[$i] === null); if ($isnull) { continue; } $class = $param->getClass(); if ( $class !== null ) { if ( !($params[$i] instanceof $class) ) { throw new ReflectionException("Invald Object Parameter for '$func_string()'"); } continue; } elseif ( $param->isArray() ) { if ( !is_array($params[$i]) ) { throw new ReflectionException("Invald Array Parameter for '$func_string()'"); } continue; } elseif ( $this->useDocTypehint() ) { $reg = '/@param\s+([a-z0-9\s|_]+)\s+\$'.$param->getName().'/i'; /*
  • Si le paramètre est documenté
  • /
if ( preg_match($reg, $func->getDocComment(), $match) ) { $types = explode('|', preg_replace(self::$TYPE_SEARCH, self::$TYPE_REPLACE, strtolower(' '.$match[1].' '))); /**
  • "mixed" remplace n'importe quel type, donc si il est dans la liste, on "valide"
  • /
if ( in_array('mixed', $types) ) { continue; } /**
  • Si le paramètre est d'un des types de la liste documentée
  • /
foreach ( $types as $type ) { $istype = 'is_'.$type; if ( in_array($type, self::$TYPE_ACCEPT) && $istype($params[$i]) || class_exists($type, false) && $params[$i] instanceof $type ) { continue 2; } } /**
  • Sinon, le paramètre ne correspond pas, donc on passe à la fonction suivante
  • /
throw new ReflectionException("Invalid type from DocComment for '$func_string()'"); } continue; } elseif ( ! $param->allowsNull() ) { throw new ReflectionException("Invald Null Parameter for '$func_string()'"); } } // Check si il reste des paramètres ? if ( $scope ) { return $func->invokeArgs($scope, $params); } else { return $func->invokeArgs($params); } } catch ( ReflectionException $e ) { // echo pour les débugs. // echo '<strong>'.$func_string.':'.$e->getMessage().'</strong><br />'; continue; } /**
  • S'il y a une exception, on la relaye
  • /
catch ( Exception $e ) { throw $e; } } // end foreach ($funcs as $func_string) throw new Exception('No valid overload function/method found'); } /**
  • Simule la surcharge d'une fonction ou d'une méthode.
*
  • Fonctionne de manière similaire à {@link overloadMatchArgs()}
  • ormis le fait qu'il n'y ait pas besoin de spécifier d'arguments,
  • ceux-ci étant récupérés depuis le contexte où est appelé overloadMatch.
*
  • {@example ../demo/syntaxExemple01.php}
*
  • C'est pour cette raison, qu'il est impératif que cette méthode
  • soit appelée depuis une fonction ou une méthode.
  • Dans le cas contraire, une exception sera lancée.
* *
  • @param string $pattern Le masque de validation
  • @return mixed
  • @thrown
  • /
final public function overloadMatch($pattern) { try { $params = debug_backtrace(); if ( !isset($params[1]) ) { throw new Exception('Unable to get arguments: Invalid Scope ?', E_USER_WARNING); } return $this->overloadMatchArgs($pattern, $params[1]['args']); } catch ( Exception $e ) { throw $e; } } /**
  • Simule la surcharge d'une fonction ou d'une méthode en spécifiant l'argument
  • que l'on doit lui passer.
*
  • Appelle la première fonction/méthode qui satisait le masque $pattern dans le
  • {@link setScope() champs d'action} défini et dont la signature correspond
  • aux paramètres $params.
* *
  • @param string $pattern Le masque de validation
  • @param mixed $param Un argument à passer à la fonction de surcharge
  • @return mixed
  • @thrown
  • /
final public function overloadMatchArg($pattern, $param) { try { return $this->overloadMatchArgs($pattern, array($param)); } catch ( Exception $e ) { throw $e; } } /**
  • Simule la surcharge d'une fonction ou d'une méthode en spécifiant les arguments
  • que l'on doit lui passer.
  • Appelle la première fonction/méthode qui satisait le masque $pattern dans le
  • {@link setScope() champs d'action} défini et dont la signature correspond
  • aux paramètres $params.
* *
  • ATTENTION: Si vous ne passez qu'un seul paramètre $params et que celui-ci est un tableau,
  • le comportement du script risque d'être imprévisible. Ainsi, si vous n'avez qu'un paramètre
  • à donner, passez plutôt par {@link overloadArg()}.
  • @param string $pattern Le masque de validation
  • @param array $params Un tableau d'arguments
  • @return mixed
  • @thrown
  • /
final public function overloadMatchArgs($pattern, $params) { try { if ( !is_array($params) || func_num_args() > 2 ) { $params = func_get_args(); array_shift($params); } $scope = $this->getScope(); if ( $scope ) { $funcs = preg_grep($pattern, get_class_methods($scope)); } else { $list = get_defined_functions(); if ( $this->useInternalMatch() ) { $list['user'] = array_merge($list['user'],$list['internal']); } /* On retourne la liste des fonctions qui correspondent au masque La liste retournée par get°defined_functions étant en minuscules, on rajoute le flag "i" (insensible à la casse)
  • /
$funcs = preg_grep($pattern . 'i', $list['user']); } if ( empty($funcs) ) { throw new Exception("Unknown function or method. No matches with pattern '$pattern'"); } return $this->overloadArgs($funcs, $params); } catch ( Exception $e ) { throw $e; } } public function __call ($func, $params) { if ( !isset($this->_patterns[$func]) ) { throw new Exception("Unknow function or method '$func()'"); } try { if ( is_array($this->_patterns[$func]) ) { return $this->overloadArgs($this->_patterns[$func], $params); } elseif ( is_string($this->_patterns[$func]) ) { $pattern = str_replace('__FUNCTION__', $func, $this->_patterns[$func]); return $this->overloadMatchArgs($pattern, $params); } throw new Exception("Invalid overload for '$func()'"); } catch ( Exception $e ) { throw $e; } return false; } } ?>

Conclusion :


Je sais que c'est encore perfectible mais je suis ouvert à toute proposition/suggestion ^^.
Vu que je n'ai pas un accès rgulier au net, je profite du fait d'être connecté pour poster cette source et en avoir des retours.

Merci.

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.