Ainsi, l’exemple suivant ne fonctionne pas avec PHP 5.3, et ce n’est pas parce qu’il est complètement stupide :
<?php class foo { public $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } } $foo = new foo(); $bar = $foo->getBarDumper(); $bar(); // Fatal error: Using $this when not in object context in /path/to/stupid/script.php on line X ?>
Pour accéder à l’instance de la classe dans laquelle était déclarée la fonction anonyme avec PHP 5.3, il était donc nécessaire de recourir à une variable temporaire et de l’utiliser dans une fermeture lexicale.
<?php class foo { public $bar = 'A bar'; public function getBarDumper() { $that = $this; return function() use ($that) { var_dump($that->bar); }; } } $foo = new foo(); $bar = $foo->getBarDumper(); $bar(); // string(5) "A bar" ?>
Reste que cette solution a un inconvénient majeur, car vu que la fonction anonyme n’a pas la même portée que la méthode, $that ne peut accéder aux propriétés et méthodes protégées ou privées de la classe.
<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { $that = $this; return function() use ($that) { var_dump($that->bar); }; } } $foo = new foo(); $bar = $foo->getBarDumper(); $bar(); // Fatal error: Cannot access protected property foo::$bar in /path/to/stupid/script.php on line X ?>
Pour remédier à cela, l’implémentation des fonctions anonymes a été revue pour PHP 5.4.
Depuis cette version, si une fonction anonyme est déclarée dans une méthode, elle a la portée de la classe et $this
représente dans cette fonction l’instance courante de la classe et le code suivant est donc pleinement fonctionnel avec PHP 5.4 :
<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } } $foo = new foo(); $bar = $foo->getBarDumper(); $bar(); //string(5) "A bar" ?>
Corollaire de ce qui précède, en PHP 5.4, il peut y avoir des fonctions anonymes statiques.
Je vous rassure immédiatement, si vous n’avez pas compris la phrase précédente, c’est parfaitement normal.
Voici un exemple pour vous aider :
<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } public static function getFactory() { // la fonction anonyme suivante est statique // car elle est déclarée dans une méthode statique. return function() { return new static(); }; } } $factory = foo::getFactory(); $foo = $factory(); $bar = $foo->getBarDumper(); $bar(); //string(5) "A bar" ?>
En clair, une fonction anonyme déclarée dans une méthode statique est une fonction anonyme statique, et en conséquence et fort logiquement, il n’est pas possible d’accéder à $this
dans ce type de fonction anonyme.
Pour preuve, le code suivant génère une erreur :
<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } public static function getFactory() { // la fonction anonyme suivante est statique // car elle est déclarée dans une méthode statique. return function() { var_dump($this); return new static(); }; } } $factory = foo::getFactory(); $foo = $factory(); $bar = $foo->getBarDumper(); $bar(); // Notice: Undefined variable: this in /path/to/stupid/script.php on line X // NULL // string(5) "A bar" ?>
Tout cela n’aurait guère d’intérêt si PHP 5.4 ne permettait pas de modifier l’objet vers lequel pointe $this
dans une fonction anonyme ainsi que la portée de la classe à l’aide des méthodes \closure::bind()
et \closure::bindTo()
:
<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } } class bar { protected $bar = 'A fucking bar!'; } $foo = new foo(); $bar = new bar(); $barDumper = $foo->getBarDumper(); $barDumper = $barDumper->bindTo($bar, $bar); $barDumper(); // string(14) "A fucking bar!" ?>
Si vous utilisez \closure::bind()
et \closure::bindTo()
sur des fonctions anonymes non statiques, vous n’aurez aucun problème.
Par contre, si vous le faites sur des méthodes statiques, PHP va vous insulter :
Or, PHP ne semble à première vu ne pas permettre de discriminer entre une fonction anonyme statique et une fonction anonyme non statique, ce qui peut poser un problème dans certains cas, notamment lorsque l’on manipule des fonctions anonymes définies par un tiers.<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } public static function getFactory() { // la fonction anonyme suivante est statique // car elle est déclarée dans une méthode statique. return function() { var_dump($this); return new static(); }; } } class bar {} $factory = foo::getFactory(); $bar = new bar(); $factory = $factory->bindTo($bar, $bar); Warning: Cannot bind an instance to a static closure in /path/to/stupid/script.php on line X ?>
Heureusement, comme souvent avec PHP, il y a une solution, même si, toujours comme souvent avec PHP, elle est immonde.
Une fonction anonyme, avant d’être une instance de la classe \closure
, est une fonction.
La classe \reflectionFunction
permet donc de l’analyser pour en extraire des informations, et il se trouve que cette classe dispose d’une méthode très intéressante dans le cas qui nous occupe, à savoir \reflectionFunction::getClosureThis()
.
Comme son nom peut vous le laisser supposer, cette méthode permet de récupérer la valeur de $this
dans le cadre de la fonction anonyme.
Pour savoir s'il est possible d'utiliser \closure::bindTo()
sur une fonction anonyme, il suffit donc de faire appel à cette méthode et de comparer sa valeur de retour.
Si elle est égale à NULL
, la fonction anonyme est statique et en conséquence, l'utilisation de \closure::bindTo()
sur cette fonction anonyme générera une erreur!
<?php class foo { protected $bar = 'A bar'; public function getBarDumper() { return function() { var_dump($this->bar); }; } public static function getFactory() { // la fonction anonyme suivante est statique // car elle est déclarée dans une méthode statique. return function() { return new static(); }; } } $factory = foo::getFactory(); $reflectedFactory = new \reflectionFunction($factory); if ($reflectedFactory->getClosureThis() === null) echo "La fonction anonyme est statique" . PHP_EOL; $bar = $factory()->getBarDumper(); $reflectedBar = new \reflectionFunction($bar); if ($reflectedBar->getClosureThis() !== null) echo "La fonction anonyme n’est pas statique" . PHP_EOL; // La fonction anonyme est statique // La fonction anonyme n’est pas statique ?>
5 réactions
1 De bdelespierre - 30/07/2013, 16:37
Bon article mais je tiens à préciser les points suivants:
Les fonctions anonymes existent depuis PHP 4.0.1 qui introduit create_function (http://php.net/manual/fr/function.c...). Ce que PHP 5.3 apporte de nouveau ce sont les closures. La différence est expliquée dans mon article sur le sujet : http://bdelespierre.fr/article/de-l...
Plus que des outils, les closures sont en réalité des structures de comportement, et elles n'enlèvent rien à la complexité d'un projet. Tout au plus elles facilitent la délégation.
En PHP 5.3 l'usage de $this dans une closure ne faisait pas du tout référence à la closure. PHP considérait simplement ça comme une erreur de contexte.
La création implicite de closures statique peut aller jusqu'a produire des erreurs fatales (http://bdelespierre.fr/article/php-...) sans pour autant qu'on puisse le catcher ni le détecter. C'est là je pense le nœud du problème.
C'est une bonne idée d'avoir pensé à utiliser la réflexion pour déterminer le scope de la closure mais une closure qui n'a pas encore de contexte d'instance n'est pas forcément statique:
$f = function () { echo $this->name; };
$r = new ReflectionFunction($f);
var_dump( $r->getClosureThis() ); // affiche "null"
// pourtant $f n'est pas statique,
// la preuve:
$f = $f->bindTo((object)'name' => 'foobar');
$f(); // affiche "foobar"
De plus, il convient de rappeler que la réflexion est désastreuse en termes de performances. C'est un mécanisme à utiliser avec précaution et surtout parcimonie.
2 De mageekguy - 30/07/2013, 16:57
@bdelespierre : Bien vu pour le $this en 5.3 qui ne pointe pas vers l'instance de la \closure, il y a tellement longtemps que je l'utilise plus 5.3 que j'aurais du ne pas me fier à ma mémoire et vérifier.
Par contre, dans ton exemple, ta fonction est statique, dans le sens ou sa portée n'est pas celle d'une instance de classe.
$this
n'existe en effet pas au niveau global, tout comme dans le contexte d'une méthode statique de classe.Si tu veux discriminer entre une fonction anonyme déclarée au niveau global et une déclarée dans une méthode, tu peux utiliser
\reflectionFunction::getClosureScopeClass()
:Par contre la limitation imposée par PHP qui fait qu'il n'est pas possible de rendre non statique une fonction anonyme déclarée dans une méthode statique de classe me semble complètement artificielle et stupide, et pas uniquement parce qu'il est possible de le faire pour une fonction anonyme déclarée au niveau global.
C'est en effet une possibilité qui permettrait de faire (par exemple) de l'injection de dépendance très facilement.
Quand aux performances soit-disant déplorable de l'introspection, je demande à voir, d'autant que dans le cas présent, les alternatives sont peu nombreuses.
Et create_function() était juste une vaste blague, à tel point que je ne serais pas surpris qu'elle ait été ajoutée au code de PHP un 01/04.
3 De bdelespierre - 31/07/2013, 11:36
@mageekguy Je suis bien d'accord, c'est stupide d'empêcher la définition d'un contexte d'instance pour une closure statique. De plus, quid de la classe de service dont le boulot est de fournir des closures génériques afin de faciliter la délégation ?
Dans mon exemple, ma closure n'est en réalité pas statique du tout au niveau de l'engine mais sémantiquement, c'est vrai qu'on peut dire qu'elle l'est. En fait, le Zend Engine utilise un flag (voir https://github.com/php/php-src/blam...) pour différentier les closures statiques des autres. Le malheur c'est que ce flag n'est pas exposé au niveau de l'API... En fait, jusqu’à ce qu'on lui attache un contexte, une closure définie dans le scope global (ou d'ailleurs n'importe quel scope en dehors de celui d'un objet ou d'une classe) n'est ni statique ni non-statique au niveau du moteur.
C'est vrai aussi qu'on a pas beaucoup d'alternatives à l'usage de la réflexion mais fais donc tourner quelques tests et tu verras ce que ça coûte en termes de charge. De toute façon, quand on en est réduit à faire de la réflexion c'est qu'on a foiré quelque chose en général et qu'on essaie de tordre le comportement normal de PHP. Le problème avec les closures, c'est justement que le comportement normal est anormal.
Quand a create_function, c'est effectivement du grand n'importe quoi au niveau du Zend Engine mais c'est toujours mieux qu'eval non ?
4 De mageekguy - 31/07/2013, 12:04
@bdelespierre : à propos de eval(), comme ça, juste pour le fun, que crois-tu que font include et require lorsqu'elles sont appelées ?
La fonction eval() n'est pas le Mal, ce qui est le Mal, c'est de l'utiliser sans prendre de précaution notamment lorsque les données utilisées pour générer le code qui lui est passé en argument proviennent de l'extérieur.
5 De bdelespierre - 31/07/2013, 14:02
@mageekguy : Je n'ai pas dit "le Mal", j'ai dit qu'une fonction définie au runtime était, dans tous les cas, une meilleure alternative que l'évaluation de code, même si au niveau de l'engine c'est pareil. La différence ? Le scope !