Paramètres nommés en PHP 5 et 7
Divulgâchage : Contrairement à de nombreux languages, le PHP (avant la version 8) ne permet pas le passage de paramètres nommés. Voici un adapteur générique pour ajouter cette possibilité au langage. On va définir un adaptateur qui fera la traduction entre le code appelant avec un seul tableau et vos méthodes et leurs (nombreux) paramètres.
Il y a quelques temps, je relisais un morceau de code et je suis tombé sur une façon originale de passer les paramètres, en utilisant un tableau. Quelque chose de ce genre :
$res = $obj->method(["foo" => "bar"]) ;
Comme c’est la première fois que je rencontre cet idiome dans ce code, je vais voir la fameuse méthode que je trouve facilement et qui, en gros, fait comme suit :
class SomeClass
{
public function method($params)
{// Get Parameters
$foo = $params["foo"] ?: "default" ;
$bar = $params["bar"] ?: "default" ;
// Compute result
return $foo . $bar ;
}
}
La vraie méthode faisait en réalité 500 lignes, avec des
if/else
plutôt que l’opérateur?:
et utilisait une dizaine de paramètres, pas très photogénique. Là, je vous ai fait une version plus lisible.
Je m’étonne que cette fonction ne déclare pas simplement deux paramètres avec leurs type et leur valeurs par défaut et j’imagine déjà tous les risques en matière de maintenance qu’elle contient…
- Comment savoir facilement quels sont les paramètres valides vu qu’il n’y a pas de documentation ? Comment s’assurer que la documentation soit à jour d’ailleurs ?
- Comment s’assurer que les codes appelant cette méthode soient à jour si cette liste de paramètres venait à changer ?
- Que devrait-il se passer si un appelant ajoute des paramètres non prévus ?
- Si des vérifications de type sont nécessaires, il faut les ajouter, ce qui complexifie le code…
Du coup, parce que je suppose que l’auteur de ces lignes avait de bonnes raisons pour les écrire comme il l’a fait, je vais à la chasse aux informations et fini par trouver la raison de cet idiome :
C’est pour les passer en les nommant, comme en python. Je préfèrerais écrire
method(foo: "bar")
mais php le permet pas.Tintin (prénom d’emprunt)
Et oui, contrairement à d’autres langages comme python
,
C#
et d’autres, le PHP
(5 et 7) ne permet pas
de passer les paramètres en les nommant, ce qui peut manquer quelques
fois.
Alors sans plus attendre, voici une méthode pour ajouter cette
possibilité à PHP
sans toucher aux fonctions appelées !
Exemple de classe à adapter
Avant de commencer, voici un exemple de classe PHP
que
nous allons adapter. Le seul but de cette classe étant de servir
d’exemple, ses méthodes ne font rien d’utile.
class Sample
{
public static function logMsg($foo = "foo", $bar = "bar", $method = null)
{echo ($method ?: "logMsg") ;
echo " - foo => $foo" ;
echo " - bar => $bar\n" ;
}
public $foo ;
public $bar ;
public function __construct($foo = "foo", $bar = "bar")
{$this->foo = $foo ;
$this->bar = $bar ;
self::logMsg($foo, $bar, "__construct") ;
}
public function doStuff($foo = "foo", $bar = "bar")
{self::logMsg($foo, $bar, "doStuff") ;
}
public function __toString()
{return "{foo : {$this->foo}, bar : {$this->bar}}" ;
}
}
Adapter les méthodes
Commençons donc par la méthode commune à tous les cas, et qui se charge du cœur du sujet : gérer le tableau fourni pour appeler la méthode qu’on veut.
Pour ça, on va utiliser l’API de réflexion de PHP et en particulier, la classe ReflectionMethod qui permet de manipuler les méthodes comme si c’étaient des objets.
function invokeWithArray($method, $instance, $parameters)
{$args = [] ;
foreach ($method->getParameters() as $param) {
$name = $param->getName() ;
if (isset($parameters[$name])) {
$args[$name] = $parameters[$name] ;
unset($parameters[$name]) ;
else if ($param->isDefaultValueAvailable()) {
} $args[$name] = $param->getDefaultValue() ;
else {
} throw new \Exception("parameter missing") ;
}
}
if (count($parameters) > 0) {
throw new \Exception("Too many arguments") ;
}
// Appel de la méthode avec les paramètres
return $method->invokeArgs($instance, $args) ;
}
Bien sûr, on pourrait imaginer améliorer cette fonction en lui
ajoutant la gestion des types (via quelque chose du genre
instance of $params->getType()
) mais ça alourdi
l’exemple et sera de toutes façons fait lors de l’appel à
invokeArgs()
.
Avec ça, vous pouvez déjà appeler n’importe quelle méthode. Vous
récupérez la ReflectionMethod
correspondante puis faites
votre appel.
$refClass = new ReflectionClass(get_class($obj) ;
$refMethod = $refClass->getMethod("method") ;
$res = invokeWithArray($refMethod, $obj, ["foo" => "bar"]) ;
Mais ça n’est pas encore ça. Si le but était de simplifier, c’est un peut raté… On va donc améliorer le système.
Adapter les classes
Pour adapter facilement n’importe quelle classe, on va définir une
classe abstraite qui servira de base à toutes les autres. Et
pour éviter de devoir définir toutes les méthodes possibles, nous
utilisons les méthodes
magiques du PHP
, à savoir :
__call()
qui est appelée lorsqu’une méthode appelée sur un objet n’existe pas. Le premier paramètre est le nom de la méthode, le deuxième est le tableau contenant les paramètres.__callStatic()
qui fait la même chose mais pour les méthodes statiques de classe.__invoke()
qui permet d’utiliser l’objet comme étant une fonction. Je m’en sert pour déréférencer l’adaptateur et obtenir l’objet adaptés, juste au cas où.
abstract class ArrayAdapter
{
abstract public static function getReflectionClass() ;
private $target ;
public function __construct($args)
{$class = static::getReflectionClass() ;
$this->target = $class->newInstanceWithoutConstructor() ;
$ctor = $class->getConstructor() ;
$ctor, $this->target, $args) ;
invokeWithArray(
}
public function __call($name, $args)
{if (count($args) != 1) {
throw new \Exception("Wrong number of arguments") ;
}$class = static::getReflectionClass() ;
$method = $class->getMethod($name) ;
return invokeWithArray($method, $this->target, $args[0]) ;
}
public static function __callStatic($name, $args)
{if (count($args) != 1) {
throw new \Exception("Wrong number of arguments") ;
}$class = static::getReflectionClass() ;
$method = $class->getMethod($name) ;
return invokeWithArray($method, null, $args[0]) ;
}
public function __invoke()
{return $this->target ;
}
}
Pour construire un adaptateur pour une classe (i.e. celle
d’exemple), il suffit donc de créer une classe qui étende le
ArrayAdapter
et de définir la méthode
getReflectionClass()
. Quelque chose dans ce goût là :
class AdaptedSample extends \ArrayAdapter
{public static function getReflectionClass() {
return new \ReflectionClass("Sample") ;
} }
C’est déjà mieux mais si on doit créer une nouvelle classe à chaque
fois, on va jamais s’en sortir. C’est là où le PHP
dépasse
beaucoup de langages, on peut automatiser tout ça.
Métaprogrammation
Pour adapter n’importe quelle classe sans devoir écrire de code, nous
allons utiliser trois autres fonctionnalités très pratique du
PHP
:
eval() Souvent considérée comme (très) dangereuse pour la sécurité, on peut quand même l’utiliser avec parcimonie pour ajouter de la magie à nos codes…
L’idée est donc de définir un espace de nom dédié aux adaptateurs
(e.g. \\Adapted\\
dans notre cas) dans lequel les
classes seront construites automatiquement lorsqu’on en a besoin.
Encore une fois, ce code est simplifié. En vrai, je fais une vérification que la classe à adapter existe et plutôt que « éval()uer » une chaîne, j’écris le template dans un fichier à part que j’inclus tout en récupérant le résultat via une temporisation de sortie. Mais restons simples pour l’instant.
spl_autoload_register(function ($name) {
if (strpos($name, "Adapted") !== 0) {
return ;
}
$parts = explode("\\", $name) ;
$classname = array_pop($parts) ;
$namespace = implode("\\", $parts) ;
$targetClass = str_replace("Adapted\\", "", $name) ;
// Here be dragons
eval("
namespace $namespace ;
class $classname extends \ArrayAdapter
{
public static function getReflectionClass() {
return new \ReflectionClass('$targetClass') ;
}
}
") ;
; })
Avec ça, il n’est même plus nécessaire d’écrire du code pour adapter vos méthodes, l’autoloader s’en charge pour vous :).
use Adapted\Sample ;
use Adapted\Sample ;
$a = new Sample(["bar" => "new"]) ;
// __construct - foo => foo - bar => new
$a->doStuff(["bar" => "new"]) ;
// doStuff - foo => foo - bar => new
Sample::logMsg(["bar" => "new"]) ;
// logMsg - foo => foo - bar => new
echo $a() . "\n";
// {foo : foo, bar : new}
Et après ?
L’intérêt des langages interprétés sur les langages compilés est, entre autre, leur facilité d’introspection. Comme on le voit ici, il est possible d’écrire du code qui va s’adapter automatiquement à n’importe quel code.
Dans le cas qui nous occupe, ça nous permet d’ajouter une fonctionnalité manquante au langage, le passage de paramètres par leurs noms. On peut alors les passer dans l’ordre qu’on veut et le code appelant est plus lisible puisque le nom du paramètre est indiqué directement.
Mais cette possibilité des paramètres nommés est pour moi dangereuse car elle masque un problème plus profond de maintenabilité en facilitant la définition de méthodes ayant un grand nombre de paramètres. Ce type de programmation ne devrait donc être utilisée que si aucun nettoyage du code n’est possible.