PHP5.4: Erweiterte Closures
Die Beta1 von PHP5.4 habe ich hier nicht auch noch angekündigt, das haben andere schon zur Genüge getan und wirklich was Neues dazu gekommen, ist ja nun auch nicht. Denkste, stellte ich heute fest, denn fast unscheinbar gibt es in der NEWS einen kleinen Eintrag:
15 Sep 2011, PHP 5.4.0 Beta
- Implemented closure rebinding as parameter to bindTo. (Gustavo Lopes)
Als ich das gelesen habe, dachte ich mir: “Is ja niedlich, endlich geht das lange überfällige
feature function () use ($this) {}
(im Objekt-Kontext), aber so einfach ist das nun doch
nicht. Heute bin ich auf
eine Mail von der Internals-Mailinglist
gestoßen.
- Closures can now have an associated scope
- Closures can now have a bound object
Closures can now be either static or non-static
- Closures defined in a place with an active scope are scoped accordingly
- Closures defined inside an instance method (or bound closure) have a bound object (namely $this)
- Closures are static if defined within a static method or with the static keyword (static function() { … }) static => !bound (but it’s false that !static => bound) bound => scoped !static && scoped => bound (i.e., if !static, scoped < => bound)
The bound instance and the scoped can be freely changed with Closure::bind and $closure->bindTo, subject to these constraints.
“Statische Closures”? Nanu? Das musste ich mir nun doch noch etwas genauer anschauen, aber die Beschreibung im RFC ist auch nicht sooo dermaßen hilfreich. Also einfach mal bisschen rumgespielt und das bisherige Fazit ist ganz nett ausgefallen
1 . function () use ($this) {}
geht tatsächlich weiterhin nicht. Stattdessen werden
nun alle Closures an den Sichtbarkeitsbereich (“Scope”) gebunden, in dem sie definiert wurden.
<?php
class A {
public function getClosure () {
return function () {
echo var_dump($this);
};
}
}
$a = new A;
$f = $a->getClosure();
$f();
// object(A)#1 (0) {
// }
- Will man dieses Verhalten explizit nicht, kann man jetzt auch statische Closures (d.h. Closures ohne Bindung zu einem Scope) erstellen
<?php
class A {
public function getClosure () {
return static function () {
echo var_dump($this);
};
}
}
$a = new A;
$f = $a->getClosure();
$f();
// NULL
- Der Vorteil davon ist logischerweise, dass die Closures nun auch auf
protected
- undprivate
-Methoden/-Eigenschaften zugreifen können
<?php
class A {
public function getClosure () {
return function () {
$this->sayHello();
};
}
private function sayHello () {
echo "Hello World\n";
}
}
$a = new A;
$f = $a->getClosure();
$f();
// Hello World
Ergibt auch Sinn, sonst gäbe es ja keinen Vorteil zu bisherigen “Workarounds” (was jetzt zwar auch funktioniert, aber überflüssig geworden ist).
<?php
class A {
public function getClosure () {
$that = $this;
return function () use ($that) {
$that->sayHello();
};
}
private function sayHello () {
echo "Hello World\n";
}
}
$a = new A;
$f = $a->getClosure();
$f();
// Hello World
Etwas unheimlich ist das letzte Beispiel, dass heraus kommt, wenn man
das RFC
etwas durchschaut hat, denn die Zuweisung von $this
und die Bestimmung des
Sichtbarkeitsbereiches geschieht nicht – wie man annehmen mag – “automatisch” wärend
der Erstellung, sondern durch die fast schon traditionelle Magie über spezielle Methoden.
Zur Erinnerung: Ein Closure ist genau genommen ein Objekt der Klasse Closure
, dass seinerseits
__invoke()
so implementiert, dass der Code des Closures ausgeführt wird, und wenn man nun
“die Klasse” als Funktion aufruft, wird wie von Geisterhand Closure::__invoke()
aufgerufen
(das geht auch mit eigenen Klassen, die __invoke()
implementieren). Genau genommen ist
$x = function () {}
also nur eine Instanzierung einer Klasse mit einem kleinen Extra. Für
das neue Feature implementiert Closure
nun zusätzlich bind($to[, $scope])
(bzw statisch
bindTo($closure, $to[,$scope])
), wobei die Methode ein Klon des Closures liefert, das an
$to
gebunden ist und die Sichtbarkeit auf $scope setzt. Klingt etwas verwirrend, deshalb ein Beispiel
<?php
class A {
public function getClosure () {
return function () {
$this->sayHello();
};
}
}
class B {
private function sayHello () {
echo "Hello World\n";
}
}
class C extends B {
}
$a = new A;
$c = new C;
$f = $a->getClosure();
$g = $f->bindTo($c, 'B');
$g();
// Hello World
Der erste Parameter setzt $this = $c
, der zweite sorgt dafür, dass sich der Closure
im Kontext der Klasse B
befindet und somit auf alle privaten und geschützten Elemente
von B
zugreifen kann. Unheimlich ist es deshalb, weil es nun
ohne Verrenkungen möglich
ist die Kapselung einzelner Klassen aufzubrechen und Code direkt zu injezieren. Aber wer wird
schon in seiner eigenen Anwendung seine Klassen “hacken” ;)? Speziell für Unit-Tests kann das
sicherlich auch sehr sinnvoll sein, weil das nun testen von privaten und geschützten Methoden
auffällig erleichert
<?php
$f = function () {
return $this->privateMethodToTest();
};
$g = $f->bind($testObject, 'Classname');
$this->assertEquals('Expected', $g());