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)

Quelle

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.

Quelle

“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) {
// }
  1. 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
  1. Der Vorteil davon ist logischerweise, dass die Closures nun auch auf protected- und private-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());