Felix Riesterer: Lese ich das richtig? PHP-Objekte ähnlich flexibel wie in JS?

Liebe Fachleute,

heute las ich auf heise.de über PHP5.3 und die darin enthaltenen neuen Features, darunter z.B. Closures und anonyme Funktionen usw. Bedeutet das, dass man in PHP5.3 ähnlich wie in JavaScript Objekte zur Laufzeit erweitern und verändern kann? Im von mir bisher begriffenen Klassen-basierten System (ich stehe in etwa auf dem Stand von PHP4.4 was OO in PHP angeht) war das so nicht möglich, jedenfalls nicht so dynamisch...

Liebe Grüße,

Felix Riesterer.

--
ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
  1. Hallo Felix,

    heute las ich auf heise.de über PHP5.3 und die darin enthaltenen neuen Features, darunter z.B. Closures und anonyme Funktionen usw. Bedeutet das, dass man in PHP5.3 ähnlich wie in JavaScript Objekte zur Laufzeit erweitern und verändern kann?

    Jain. Um mal etwas aus der internen Perspektive zu erzählen (ich war an der Implementierung von Closures in PHP beteiligt): Es gab ziemlich viele Diskussionen auf der PHP-Mailingliste, wie man Closures dazu verwenden kann, um Objekte zu erweitern. Es wurde aber festgestellt, dass dies zu vielen Problemen führt, wenn man das mit anderen PHP-OOP-Semantiken verbindet (Visibility wie private/protected/public was es in JS so direkt nicht gibt sondern nur über Closures emuliert, andere Namensräume für Variablen und Methoden in Objekten, etc.). Die verschiedenen Ansätze, die es gab, hatte ich vor langer Zeit mal in http://wiki.php.net/rfc/closures/object-extension zusammengefasst. Nachdem auf der PHP-Mailingliste aber kein Konsens entstehen konnte, welchen Weg man wählen will, wurde entschieden, dass Closures gar keinen Zugriff auf $this haben, damit man das zu einem späteren Zeitpunkt noch nachrüsten kann (http://wiki.php.net/rfc/closures/removal-of-this).

    Allerdings: Es gibt bestimmte Dinge, die Du bereits jetzt machen kannst:

    Python-artige Variante mit $self als ersten Parameter:

    class MeineKlasse {  
      public $begruessung = 'Hallo';  
      public function __call ($method, $args) {  
        array_unshift ($args, $this);  
        return call_user_func_array ($this->$method, $args);  
      }  
    }  
      
    $objekt = new MeineKlasse;  
    $objekt->hallo = function ($self, $wer) {  
      echo $self->begruessung." ".$wer."!\n";  
    };  
    $objekt->hallo ('Welt');
    

    Objekt als Closure-Variable:

    class MeineKlasse {  
      public $begruessung = 'Hallo';  
      public function __call ($method, $args) {  
        return call_user_func_array ($this->$method, $args);  
      }  
    }  
      
    $objekt = new MeineKlasse;  
    $objekt->hallo = function ($wer) use ($objekt) {  
      echo $objekt->begruessung." ".$wer."!\n";  
    };  
    $objekt->hallo ('Welt');
    

    Allerdings: In den so erweiterten Methoden hast Du nur Zugriff auf die Variablen/Methoden des Objekts, die public sind. Auf private/protected kannst Du so nicht zugreifen. Aber Du kannst mit dieser Logik PHP-Objekte, deren Klasse es über __call zulässt selbst beliebig erweitern mit Closures - so ähnlich wie Javascript.

    Viele Grüße,
    Christian

    --
    Mein "Weblog" [RSS]
    Using XSLT to create JSON output (Saxon-B 9.0 for Java)
    »I don't believe you can call yourself a web developer until you've built an app that uses hyperlinks for deletion and have all your data deleted by a search bot.«
                -- Kommentar bei TDWTF
    1. Hallo nochmal,

      Ich hab übrigens mal was schnell zusammengehackt, was vielleicht das Leben erleichtert. Ich behaupte nicht, dass es perfekt ist, man kann da sicher noch einige Dinge tweaken, aber es ist vielleicht ganz nett:

      ////////////////////////////////////////////////////////////////////  
      // BASISCODE, DER NICHT GEAENDERT WERDEN MUSS  
      ////////////////////////////////////////////////////////////////////  
        
      class ExtensibleObject {  
        private static $prototypeMethods = array ();  
        
        public function __call ($method, $args) {  
          if (!isset ($this->$method)) {  
            $class = get_class ($this);  
            if (!isset (self::$prototypeMethods[$class]) || !isset (self::$prototypeMethods[$class]->$method)) {  
              throw new BadMethodCallException ("Method $class::$method does not exist");  
            }  
            $method = self::$prototypeMethods[$class]->$method;  
          } else {  
            $method = $this->$method;  
          }  
          try {  
            $p = new ReflectionParameter ($method, 0);  
            if ($p->getName () == 'self') {  
              // Python-Style convention: First parameter is named $self  
              array_unshift ($args, $this);  
            }  
          } catch (ReflectionException $e) {  
            // ignore (parameter count < 0)  
          }  
          return call_user_func_array ($method, $args);  
        }  
        
        public static function __callStatic ($name, $args) {  
          $class = get_called_class ();  
          if (!isset (self::$prototypeMethods[$class]) || !isset (self::$prototypeMethods[$class]->$name)) {  
            throw new BadMethodCallException ("Method $class::$name does not exist");  
          }  
          $method = self::$prototypeMethods[$class]->$name;  
          try {  
            $p = new ReflectionParameter ($method, 0);  
            if ($p->getName () == 'self') {  
              // Python-Style convention: First parameter is named $self  
              throw new BadMethodCallException ("Non-static method $class::$name may not be called statically");  
            }  
          } catch (ReflectionException $e) {  
            // ignore (parameter count < 0)  
          }  
          return call_user_func_array ($method, $args);  
        }  
        
        public function __get ($name) {  
          if ($name == 'prototype') {  
            return static::prototype ();  
          } else {  
            $class = get_class ($this);  
            throw new RuntimeException ("Attribute $class::$name does not exist");  
          }  
        }  
        
        public static function prototype () {  
          $class = get_called_class ();  
          if (!isset (self::$prototypeMethods[$class])) {  
            self::$prototypeMethods[$class] = new stdClass;  
          }  
          return self::$prototypeMethods[$class];  
        }  
      }  
        
      ////////////////////////////////////////////////////////////////////  
      // ENDE  
      ////////////////////////////////////////////////////////////////////  
        
      // Abgeleitete Klasse  
      class MyObject extends ExtensibleObject {  
        public $welt = 'Welt';  
      }  
        
      // Etwas prototypisches Zeugs  
      $obj1 = new MyObject;  
      $obj1->prototype->hallo = function ($self) {  
        echo "Hallo ".$self->welt."\n";  
      };  
      MyObject::prototype ()->ciao = function ($self) {  
        echo "Ciao ".$self->welt."\n";  
      };  
      $obj1->prototype->tag = function () {  
        echo "Guten Tag!\n";  
      };  
      $obj1->nacht = function () {  
        echo "Gute Nacht!\n";  
      };  
      $obj2 = new MyObject;  
      $obj2->welt = 'kleine Welt';  
        
      echo "Hallo:\n===============\n";  
      $obj1->hallo ();  
      $obj2->hallo ();  
      echo "\nCiao:\n===============\n";  
      $obj1->ciao ();  
      $obj2->ciao ();  
      echo "\nTag:\n===============\n";  
      $obj1->tag ();  
      $obj2->tag ();  
      echo "\nNacht:\n===============\n";  
      $obj1->nacht ();  
      try {  
        $obj2->nacht ();  
      } catch (Exception $e) {  
        echo "Exception: ". $e->getMessage () ."\n";  
      }  
      echo "\nTag (static):\n===============\n";  
      MyObject::tag ();  
      echo "\nHallo (static):\n===============\n";  
      try {  
        MyObject::hallo ();  
      } catch (Exception $e) {  
        echo "Exception: ". $e->getMessage () ."\n";  
      }
      

      Die Ausgabe bei PHP 5.3 wäre folgende:

      Hallo:

      Hallo Welt
      Hallo kleine Welt

      Ciao:

      Ciao Welt
      Ciao kleine Welt

      Tag:

      Guten Tag!
      Guten Tag!

      Nacht:

      Gute Nacht!
      Exception: Method MyObject::nacht does not exist

      Tag (static):

      Guten Tag!

      Hallo (static):

      Exception: Non-static method MyObject::hallo may not be called statically

      Das ganze kombiniert hier schön Late Static Bindings (get_called_class (), static::) mit Closures und __call/__callStatic-Logik und demonstriert damit einige der neuen Features von PHP 5.3. Ich hoffe, der Code erklärt sich selbst. Was es noch nicht kann, was man aber sicher leicht hinzufügen könnte, wäre $x->prototype = $y->prototype (bzw. für den statischen Fall Class2::setPrototype (Class1::prototype ())) mit spezieller Klasse ExtensibleObjektPrototype für Type-Hints. Vielleicht hacke ich das bei Gelegenheit auch noch zusammen.

      Achja, falls man __get/__call in eigenen Klassen überschreibt, sollte das so aussehen:

      class MySubclass extends ExtensibleObject {  
        public function __get ($name) {  
          try {  
            return parent::__get ($name);  
          } catch (RuntimeException $e) {}  
          // eigentlicher Code fängt hier an, sobald die RuntimeException  
          // ignoriert wurde  
          return $this->foobar[$name]; // oder whatever  
        }  
        public function __call ($method, $args) {  
          try {  
            return parent::__call ($method, $args);  
          } catch (BadMethodCallException $e) {}  
          // eigentlicher Code fängt hier an, sobald die BadMethodCallException  
          // ignoriert wurde  
          return $this->foobar[$method] ($args); // oder whatever  
        }  
      }
      

      Viele Grüße,
      Christian

      --
      Mein "Weblog" [RSS]
      Using XSLT to create JSON output (Saxon-B 9.0 for Java)
      »I don't believe you can call yourself a web developer until you've built an app that uses hyperlinks for deletion and have all your data deleted by a search bot.«
                  -- Kommentar bei TDWTF
      1. Hallo Christian,

        wegen zu guten Einblicken in die Materie, laste ich Dir das mal an, bin natürlich um Antworten von jedem dankbar:

        Wie sieht es jetzt genau mit dem GC aus. Gibt dieser Speicher (endlich) auch an das OS wieder ab, während Scripte laufen und Variablen gelöscht werden? (Mein Interesse gilt da CLI-Anwendungen.)

        Gruß aus Berlin!
        eddi

        --
        “Um etwas zu erschaffen mit gutem Erfolg, muß man aufhören das zu sein, was man ist; um ganz das zu werden, was man hervorbringen will.”
        1. Hallo,

          Wie sieht es jetzt genau mit dem GC aus. Gibt dieser Speicher (endlich) auch an das OS wieder ab, während Scripte laufen und Variablen gelöscht werden? (Mein Interesse gilt da CLI-Anwendungen.)

          Den GC betrifft das nicht, bei dem geht's nur um zirkuläre Referenzen. PHP gibt ja auch bereits in 5.2 schon Speicher an das OS zurück, wenn bestimmte Bedingungen erfüllt sind - und an der Speicherverwaltung selbst hat sich nichts wesentliches geändert.

          Um zu verstehen, warum PHP bereits in 5.2 den Speicher nicht wirklich freigibt, eine kurze Einführung, wie in PHP die Speicherverwaltung funktioniert:

          Sobald Speicher benötigt wird, ruft der C-Code von PHP die Funktion emalloc() auf. Die Funktion fordert dann vom Betriebssystem einen Block Speicher an, der der Anwendung zur Verfügung stehen soll (deswegen steigt der Speicherverbrauch auch in Sprüngen an) - und fügt den Block zu der internen Liste mit zur Verfügung stehenden Blöcke hinzu. Aus diesem Block wird dann ein Teil reserviert und die Adresse davon zurückgegeben. Beispiel (Zahlen für die Blockgrößen aus den Fingern gesogen):

          0. Zustand der Speicherverwaltung und des Speichers:

          +------------+
           | Blockliste |
           +------------+
           | (leer)     |
           +------------+

          1. Aufruf von emalloc:

          char *foo = emalloc (20);

          1a. Anfordern eines größeren Blocks vom Betriebssystem

          [Notation: XXX reserviert, sonst frei]

          +------------+                 +----------------------------------------+
           | Blockliste |     +---------->|                                        |
           +------------+     |           +----------------------------------------+
           | Block 1  --|-----+
           +------------+

          1b. Reservieren des Speicherbereichs im Block und Zurückgeben des Zeigers.

          +------------+                 +----------------------------------------+
           | Blockliste |     +---------->|XXXXX                                   |
           +------------+     |           +----------------------------------------+
           | Block 1  --|-----+            ^
           +------------+                  |
                                           |
            foo ---------------------------+

          2. Weiterer Aufruf von emalloc:

          char *bar = emalloc (28);

          2a. Reservieren des Speicherbereichs im Block und Zurückgeben des Zeigers.

          +------------+                 +----------------------------------------+
           | Blockliste |     +---------->|XXXXXXXXXXXX                            |
           +------------+     |           +----------------------------------------+
           | Block 1  --|-----+            ^    ^
           +------------+                  |    |
                                           |    |
            foo ---------------------------+    |
                                                |
            bar --------------------------------+

          3. Noch ein Aufruf von emalloc:

          char *baz = emalloc (200);

          3a. Feststellen, dass der bisherige Speicherbereich zu klein ist, also noch ein Block vom OS anfordern:

          +------------+                 +----------------------------------------+
           | Blockliste |     +---------->|XXXXXXXXXXXX                            |
           +------------+     |           +----------------------------------------+
           | Block 1  --|-----+            ^    ^
           | Block 2  --|---+              |    |
           +------------+   |              |    |
                            |              |    |
            foo ---------------------------+    |
                            |                   |
            bar --------------------------------+
                            |             +------------- ... ----------------------+
                            +------------>|              ...                       |
                                          +------------- ... ----------------------+

          2a. Reservieren des Speicherbereichs im neuen Block und Zurückgeben des Zeigers.

          +------------+                 +----------------------------------------+
           | Blockliste |     +---------->|XXXXXXXXXXXX                            |
           +------------+     |           +----------------------------------------+
           | Block 1  --|-----+            ^    ^
           | Block 2  --|---+              |    |
           +------------+   |              |    |
                            |              |    |
            foo ---------------------------+    |
                            |                   |
            bar --------------------------------+
                            |             +------------- ... ----------------------+
                            +------------>|XXXXXXXXXXXXX ... XXXXXXXX              |
                                          +------------- ... ----------------------+
                                           ^
                                           |
            baz ---------------------------+

          Was passiert nun, wenn im Script eine Variable nicht mehr benötigt wird? Der Speicherbereich der Variable wird als frei gekennzeichnet. In unserem Beispiel:

          efree (foo);
          foo = NULL;

          +------------+                 +----------------------------------------+
           | Blockliste |     +---------->|     XXXXXXX                            |
           +------------+     |           +----------------------------------------+
           | Block 1  --|-----+                 ^
           | Block 2  --|---+                   |
           +------------+   |                   |
                            |                   |
            foo             |                   |
                            |                   |
            bar --------------------------------+
                            |             +------------- ... ----------------------+
                            +------------>|XXXXXXXXXXXXX ... XXXXXXXX              |
                                          +------------- ... ----------------------+
                                           ^
                                           |
            baz ---------------------------+

          Das Problem ist nun: Der Speicher des ersten Blocks kann nicht von PHP wieder an das Betriebssystem zurückgegeben werden, da dort noch die Variable bar lebt. Und das war hier nur im einfachen Beispiel - in PHP gibt es z.B. pro globaler Variable etliche Allozierungsaufrufe. Man hat also am Ende lauter Blöcke, die PHP hat, die teilweise belegt sind -> der Speicherverbrauch sinkt nicht mehr. Er sinkt nur dann, wenn Blöcke am Stück zurückgegeben werden können was eigentlich nur dann passiert, wenn man z.B. PHP als Apache-Modul betreibt können am Ende des Requests alle Blöcke, die während des Requests alloziert wurden, wieder freigegeben werden.

          Nun könntest Du fragen, warum PHP überhaupt Blöcke alloziert die größer sind als der angeforderte Bereich und nicht einfach die immer passend macht. Die Antwort darauf ist, dass es sehr langsam ist, vom Betriebssystem einen neuen Block anzufordern (fast unabhängig von der Größe des Blocks), es aber sehr schnell ist, intern Teile von Blöcken als belegt oder nicht belegt zu markieren. Außerdem müssen die Blöcke vom Betriebssystem "aligned" sein, d.h. können ihre Startadresse nur in Vielfachen der Page Size oder einer ähnlichen Größe haben - und bei 4 KB Page Size würdest Du um nur 100 Bytes zu allozieren ganze 3996 Bytes komplett verschwenden. Daher ist die gewählte Variante sicherlich besser als die vorige.

          Allerdings: Der nichtverwendete Speicher in den Blöcken steht wieder für neue Variablen in PHP zur Verfügung, d.h. der Speicherverbrauch eines PHP-Scripts geht zwar in der Regel nicht mehr nach unten - aber wenn es gut geht steigt er auch nicht mehr weiter.

          Viele Grüße,
          Christian

          --
          Mein "Weblog" [RSS]
          Using XSLT to create JSON output (Saxon-B 9.0 for Java)
          »I don't believe you can call yourself a web developer until you've built an app that uses hyperlinks for deletion and have all your data deleted by a search bot.«
                      -- Kommentar bei TDWTF
          1. Re:

            danke! :)

            Gruß aus Berlin!
            eddi

            --
            “Um etwas zu erschaffen mit gutem Erfolg, muß man aufhören das zu sein, was man ist; um ganz das zu werden, was man hervorbringen will.”
  2. Moin!

    heute las ich auf heise.de über PHP5.3 und die darin enthaltenen neuen Features, darunter z.B. Closures und anonyme Funktionen usw. Bedeutet das, dass man in PHP5.3 ähnlich wie in JavaScript Objekte zur Laufzeit erweitern und verändern kann?

    Nein, bedeutet es nicht.

    Das prototypische Objektmodell von Javascript hat mit dem Klassen- und Objektmodell von PHP nicht viel zu tun. Und abgesehen von der Tatsache, dass sich seit PHP 4 objektmäßig vom Prinzip her nicht viel verändert hat, könnte man sich natürlich schon immer Dynamik hinprogrammieren, wenn man sie wirklich braucht.

    - Sven Rautenberg