KingCrunchs kleine Welt

Gedanken über PHP und was mich sonst bewegt…

PHP5.4: Erweiterte Closures

| 4 Kommentare

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.
    class A {
        public function getClosure () {
          return function () {
            echo var_dump($this);
          };
        }
    }
    $a = new A;
    $f = $a->getClosure();
    $f();
    // object(A)#1 (0) {
    // }
  2. Will man dieses Verhalten explizit nicht, kann man jetzt auch statische Closures (d.h. Closures ohne Bindung zu einem Scope) erstellen
    class A {
        public function getClosure () {
          return static function () {
            echo var_dump($this);
          };
        }
    }
    $a = new A;
    $f = $a->getClosure();
    $f();
    // NULL
  3. Der Vorteil davon ist logischerweise, dass die Closures nun auch auf protected- und private-Methoden/-Eigenschaften zugreifen können
    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).

    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

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

$f = function () {
    return $this->privateMethodToTest();
};
$g = $f->bind($testObject, 'Classname');
$this->assertEquals('Expected', $g());

4 Kommentare

  1. Danke für’s Hacken! Mit bind()/bindTo() lässt sich bestimmt viel Unsinn anstellen. Am besten die Finger davon lassen… ;-)

  2. Auf meinem Iphone sieht dein Blog irgendwie seltsam aus.

  3. Hört sich auf jeden Fall sehr interessant an.

    http://www.php.net/manual/en/closure.bind.php
    Ich hab mir nur grad gedacht, was passiert, wenn ich einer Funktion als “newthis” $a gebe und als “newscope” den Namen einer vollständig anderen Klasse :D
    Ich weiss, ich mach gerne unfug.

    • Interessant, die Doku scheint mal geupdatet worden zu sein ;)

      Ich kann mir gut vorstellen, dass in der Konstellation nichts Beeindruckendes passiert. “newscope” definiert ja den Scope, an dem das Closure gebunden werden soll, also eigentlich nur, auf welche privaten oder geschützen Methoden, oder Eigenschaften es Zugriff haben darf. Das in dem Momente gebundene Objekt besitzt selbige aber nicht, beziehungsweise speziell es besitzt nicht die Elemente in der Implementierung, auf die “newscope” verweist. Lange Rede kurzer Sinn: Ich denke, das Closure hat dann einfach Zugriff auf Elemente, die nicht da sind.

      Ist so mein Gedankengang, man müsste das nochmal analysieren ;)

Kommentar verfassen

Bad Behavior has blocked 339 access attempts in the last 7 days.

Page optimized by WP Minify WordPress Plugin

%d Bloggern gefällt das: