Traits in PHP 5.4

Seit PHP die Objekt-Orientierung in genügendem Umfang unterstützt, gab es in Hinsicht auf die Code-Qualität von PHP-Applikationen große Fortschritte. Früher galten PHP-Entwickler als die Vandalen unter den Programmierern, da die Sprache – und dies ändert sich leider nur sehr langsam – sehr viele Möglichkeiten bot, undurchsichtigen und schlecht wartbaren Quellcode zu produzieren. Mit PHP 5 wurde ein großer Schritt in die richtige Richtung gemacht und heute ähnelt die Objekt-Orientierung von PHP der von Java sehr stark. Ende Juni 2011 wurde auf dieser Basis dann ein weiterer Schritt gewagt, der allerdings nicht von Java sondern von der aktuell immer stärker an Popularität gewinnenden Sprache Scala inspiriert wurde. Das neue Sprachfeature nennt sich Traits und ermöglicht Klassen-übergreifenden Code-Reuse ohne vertikale Vererbung. Was sich hier hochgestochen anhört, ist grundsätzlich ein relativ einfacher Mechanismus, der im Folgenden anhand eines simplen Beispiels erläutert wird.

Einführung des Beispiels

Am einfachsten stellt man sich einen Trait als ein Interface vor, das teilweise bereits die Implementierungen für seine Methoden beinhaltet. Mit Interfaces konnte man bereits vor PHP 5.4 die Funktionalität seiner Klassen bzw. Objekte definieren oder Anforderungen an Parameterobjekte stellen ohne sich dabei auf konkrete Klassen zu beschränken. Als Beispiel – welches auch später zur Erläuterung von Traits verwendet wird – diene hier das folgende Szenario: Bei der Entwicklung einer Web-Applikation sollen alle Daten, die im Frontend angezeigt werden über eine REST-ähnliche JSON-Schnittstelle abrufbar sein, um die spätere Integration der Daten in eine App für Smartphones zu erleichtern. Um die Controller der Applikation so schlank wie möglich zu halten, gibt es eine Klasse Rest\Response\Json, welche in allen REST-Controllern verwendet wird um Daten zu enkodieren und an den Client zu senden. Hier zuerst eine Möglichkeit, wie dieses Szenario in PHP 5.3-Code aussehen könnte:

use Rest\Response\Json as JsonResponse;

class UserController
{
    public function get($id = null)
    {
        $userModel = new UserModel();

        $response = null;
        if (null === $id) {
            $response = new JsonResponse($userModel->getAllUsers());
        } else {
            $user = $userModel->getUserById($id);

            // let's see if there is an user with the given ID
            if (null === $user) {
                throw new ResourceNotFoundException('There is no user with ID ' . $id . '.');
            }
            $response = new JsonResponse($user);
        }
        return $response;
    }
}

Der nächste Code-Block zeigt die Klasse Rest\Response\Json. Diese erbt von der hier nicht aufgeführten Klasse Rest\Response\Http, welche mindestens die Methode setHttpBody bereitstellt. Alle Objekte, welche in einer REST-Reponse übertragen werden sollen, müssen das Interface Json\Serializable implementieren.

namespace Rest\Response;

class Json extends Http
{
    public function __construct($data)
    {
        if (!is_array($data)) {
            $data = array($data);
        }
        $resources = array();
        foreach($data as $object) {
            if (!($object instanceof Json\Serializable)) {
                throw new InvalidArgumentException('Objects passed to a Rest\Response\Json must implement Json\Serializable.');
            }
            $resources[] = $object->toJson();
        }
        // the HTTP body is always an array containing the requested resources.
        $this->setHttpBody('[' . implode(',', $resources) . ']');
    }
}
namespace Json;

interface Serializable
{
    public function toJson();
}

Um nun User-Objekte über REST zum Client zu reichen, muss die Klasse User zwingend die Methode toJson beinhalten. Es gibt natürlich auch die Möglichkeit, Objekte direkt mit der Funktion json_encode zu serialisieren, dazu müssen die zu serialisierenden Objekt-Eigenschaften allerdings als public markiert sein. Aus Gründen der Sinnhaftigkeit dieses Beispiels und des persönlichen Programmierstils des Authors wird hier darauf verzichtet die Attribute der Klasse User als public zu markieren.

class User implements Json\Serializable
{
    protected $id = null;
    protected $username = null;
    protected $first_name = null;
    protected $last_name = null;
    protected $email = null;

    /** Accessor methods for the protected properties are omitted. **/

    public function toJson()
    {
        $data = array(
                    'id' => $this->getId(),
                    'username' => $this->getUsername(),
                    'first_name' => $this->getFirstName(),
                    'last_name' => $this->getLastName(),
                    'email' => $this->getEmail());

        return json_encode($data);
    }
}

Wenn man sich nun vorstellt, dass in der beschriebenen Web-Applikation nicht nur Benutzer-Objekte sondern möglicherweise eine Vielzahl unterschiedlicher Objekttypen per JSON-REST verfügbar gemacht werden sollen, zeigt sich, dass in allen Klassen der REST-Resourcen die Methode toJson auf sehr ähnliche Weise implementiert wird: Die Objekt-Attribute werden extrahiert und dann JSON-serialisiert zurückgegeben. Das Ergebnis: Eine größere Menge sehr ähnlicher Methoden.

Umsetzung mit Traits

Eine Möglichkeit zur Lösung des Problems ist der Einsatz eines Traits Json\Serializable (Wie auch das Interface wird der Trait nach dem Adjektiv benannt, das seine Funktionalität am eindeutigsten impliziert.). Alle Klassen, die diesen Trait implementieren erhalten die Funktionen, die in ihm definiert sind, als echte Member-Funktionen. Man könnte annehmen, dass dies der Mehrfachvererbung gleich kommt, was allerdings nicht der Fall ist, da Traits keine Attribute haben. (Aktuell ist es zwar zulässig, Attribute in Traits zu definieren, wie diese zur Laufzeit gehandhabt werden und welche Regeln bzgl. Sichtbarkeit und Überdeckung dieser Attribute gelten, ist undefiniert. Es ist zu erwarten, dass sich in diesem Punkt noch etwas ändern wird, bevor PHP 5.4 produktionsreif ist.)

Ziel des Umstiegs auf Traits war in diesem Beispiel, die vielen ähnlichen toJson-Methoden durch eine einheitliche Implementierung zu ersetzen. Wir treffen hierzu die Konvention, dass alle Objekt-Eigenschaften, die nicht mit einem Unterstrich beginnen, bei einem Aufruf von toJson serialisiert werden sollen. (Um den Beispielcode zu verkürzen, wird darauf verzichtet die entsprechende Getter-Methode für jede Eigenschaft aufzurufen. Dies wäre natürlich jedoch mittels einer einfachen Transformationsregel bzw. -konvention zu erledigen.) Die Definition des Traits Json\Serializable könnte damit wie folgt aussehen:

namespace Json;

trait Serializable {
    public function toJson()
    {
        $result = array();
        foreach($this as $property => $value) {
            // check whether the property begins with an underscore
            if ('_' !== $property{0}) {
                $result[$property] = $value;
            }
        }
        return json_encode($result);
    }
}

Um Instanzen der Klasse User nun in JSON zu serialisieren, muss diese folgendermaßen definiert werden:

class User
{
    use Json\Serializable;

    protected $id = null;
    protected $username = null;
    protected $first_name = null;
    protected $last_name = null;
    protected $email = null;

    /** Accessor methods for the protected properties are omitted. **/
}

Mit dem Stichwort use, welches im Körper der Klasse User verwendet wird, wird definiert, dass die Klasse den Trait Json\Serializable verwenden soll. Auf gleiche Weise lassen sich nun auch alle weiteren Entitätsklassen serialisierbar machen. Somit spart man sich eine Menge ähnlichen Quellcodes und man erhält zusätzlich den Vorteil, dass der Code zur Formatierung der Daten nicht mehr in den Entitätsklassen enthalten sein muss, was die Wartbarkeit der Software enorm erhöht.

Mehrere Traits in einer Klasse nutzen

Nun ist es allerdings nicht nur möglich pro Klasse einen Trait zu verwenden, sondern auch mehrere. Somit kann man einer Klasse gleich die Funktionalität mehrerer Traits hinzufügen. Als Beispiel stelle man sich vor, dass der oben beschriebene REST-Service nun nicht nur im JSON- sondern auch im XML-Format verfügbar sein soll. Dazu möchten die Entwickler wieder auf Traits bauen und implementieren den Trait Xml\Serializable:

namespace Xml;

use DOMDocument;

trait Serializable {
    public function toXml()
    {
        $doc = new DOMDocument();
        foreach($this as $property => $value) {
            // check whether the property begins with an underscore
            if ('_' !== $property{0}) {
                $doc->appendChild($doc->createElement($property, $value));
            }
        }
        return $doc->saveXML();
    }
}

In der Klasse User muss nun nur die use-Direktive erweitert werden und schon können User-Objekte auch in XML serialisiert werden:

class User
{
    use Json\Serializable, Xml\Serializable;

    protected $id = null;
    protected $username = null;
    protected $first_name = null;
    protected $last_name = null;
    protected $email = null;

    /** Accessor methods for the protected properties are omitted. **/
}

Namenskonflikte

Ein etwas heikles und viel diskutiertes Thema sind Namenskonflikte, die zwischen den Methoden zwei verschiedener Traits auftreten. Wenn nämlich zwei Traits, die jeweils eine gleichnamige Methode implementieren, von einer Klasse ohne Konfliktbehandlung verwendet werden, wird von PHP ein Fatal Error generiert. Zur Konfliktbehandlung wurde die use-Direktive um eine spezielle Syntax erweitert, mit der man eine von zwei Methoden priorisieren kann und Aliase für Methodennamen festlegen kann. Ein Beispiel für diesen Fall ist die folgende Entitätsklasse File:

class File
{
    use FileSystem\File, ORM\Persistable;

    protected $filename = null;
    protected $description = null;
    protected $content = null;

    public function getFilename()
    {
        return $this->filename;
    }

    public function setFilename($name)
    {
        $this->filename = $name;
    }

    public function getContent()
    {
        if (null === $this->content) {
            $this->content = $this->load();
        }
        return $this->content;
    }

    public function setContent($content)
    {
        $this->content = $content;
    }

    /** Accessor methods for the property $description are omitted. **/
}

Die Klasse File wird in einer Applikation dazu verwendet, Dateien zu repräsentieren, die der Benutzer hochladen kann und die von der Applikation einerseits im Dateisystem abgelegt werden, andererseits aber auch durch einen Eintrag in der Datenbank dargestellt werden. Nun sollen die beiden Traits FileSystem\File und ORM\Persistable verwendet werden um diese beiden Funktionalitäten zu gewährleisten.

namespace FileSystem;

trait File
{
    public function save() 
    {
        $content = $this->getContent();
        if (null === $content) {
            $content = "";
        }
        file_put_contents($this->getFilename(), $this->getContent());
    }

    public function load()
    {
        return file_get_contents($this->getFilename());
    }

    public abstract function getFilename();
}

Der Trait FileSystem\File zeigt noch ein weiteres Feature von Traits: Wie in Interfaces kann es auch in Traits nicht-implementierte Methoden geben, die wie auch in Klassen durch das Schlüsselwort abstract gekennzeichnet werden.

namespace ORM;

trait Persistable
{
    public function save() 
    {
        /** Code omitted **/
    }
}

Wenn man versucht die Klasse File mit den beiden Traits so zu laden, erhält man von PHP die folgende Fehlermeldung:

Fatal error: Trait method save has not been applied, because there are collisions with other trait methods on File […]

Um nun den offensichtlichen Konflikt zwischen den beiden save-Methoden zu beheben, muss man den neuen Operator insteadof verwenden und eine der beiden Methoden auswählen. Dadurch übernimmt PHP nur diese eine Methode in die Klasse File und lässt die andere fallen. Um die Funktionalität der verworfenen Methode dennoch zur Verfügung zu haben, muss ein Alias für diese definiert werden. Der folgende Code-Abschnitt zeigt die Klasse File inklusive Konfliktbehandlung und der Einführung des Aliases saveToFileSystem für die Methode FileSystem\File::save.

class File
{
    use FileSystem\File, ORM\Persistable {
        ORM\Persistable::save insteadof FileSystem\File;
        FileSystem\File::save as saveToFileSystem;
    }

    protected $filename = null;
    protected $description = null;
    protected $content = null;

    public function getFilename()
    {
        return $this->filename;
    }

    public function setFilename($name)
    {
        $this->filename = $name;
    }

    public function getContent()
    {
        if (null === $this->content) {
            $this->content = $this->load();
        }
        return $this->content;
    }

    public function setContent($content)
    {
        $this->content = $content;
    }

    /** Accessor methods for the property $description are omitted. **/
}

Weiterführende Literatur

Alles in allem sind Traits eine schöne Erweiterung der Objektorientierung in PHP, sollten allerdings mit Vorsicht genossen werden. Sie können auf der einen Seite natürlich wunderbar dazu beitragen, Code-Duplikate zu elimieren und Modularität zu gewährleisten, andererseits düfen sie nicht mit Mehrfachvererbung verwechselt werden und sollten auch nicht in exzessivem Umfang verwendet werden, da dies schnell zu einem nur schwer nachvollziehbarem Gesamtergebnis führt.

Als weiterführende Literatur kann ich folgende Links empfehlen:

  • Die technische Spezifikation von Traits in PHP lassen sich gut in der PHP-Dokumentation nachlesen.
  • Um einen Einblick in die Planung des Trait-Supports zu erlangen, bietet sich der RFC zu Traits in PHP an.
  • Eine kurze allgemeine Erklärung zu Traits und eine Liste von Publikationen zu diesem Thema befindet sich auf der Webseite der Software Composition Group der Universität Bern.
Avatar-Foto

Von Paul Seiffert

Paul ist seit Oktober 2010 ein Vollmatrose der Mayflower und arbeitet in einem Team von 6 Entwicklern als Lead-Architekt und stellvertretender Teamleiter. Zuvor studierte er an der TU München Informatik mit den Schwerpunkten Software-Engineering und Algorithmik. Sein Tech-Set umfasst sowohl PHP- als auch Javascript-Technologien wie Zend Framework 1, Symfony2, Node.js und Backbone.js. Paul ist immer an neuen Technologien und Vorgehensweisen interessiert und schreibt gerne über Neuerlerntes. Einige seiner Artikel sind hier auf http://blog.mayflower.de zu finden. Twitter: @SeiffertP Github: seiffert

5 Kommentare

  1. Ganz nett geschriebenes Tutorial, aber wenige Feinheiten muss ich bemängeln: Warum erbt Json von HTTP? Eine Vererbung muss immer mit „… ist ein …“ erklärbar sein. Und Json und HTTP sind völlig verschiedene Dinge, auch wenn es unter dem Aspekt des Themas „Response“ geht.
    Warum nutzt du für php-native Funktionen nicht den Root-Namespace, wenn du schon für dieses Beispiel komplett auf Namespaces setzt?

    1. Hallo Daniel,
      danke erst einmal für das Feedback! Zu deinen zwei Punkten: In dem Beispiel erbt nicht Json von PHP sondern Rest\Response\Json von Rest\Response\Http, und ich denke schon , dass man die Json-Response als eine Spzialisierung der Http-Response ansehen kann. Wo ich Dir Recht geben muss, ist, dass diese Subtypisierung nicht zu 100% das Liskovsche Substitutionsprinzip erfüllt (z.B. wegen der Exception im Konstruktur.). Wahrscheinlich wäre an dieser Stelle eine Komposition bzw. der Einsatz einer Strategy zur Formatierung des HTTP-Bodys angebracht gewesen. Ich habe beim Erstellen des Beispiels einfach mehr Wert auf das Traits-Konzept gelegt, sehe deine Kritik allerdings auf jeden Fall ein!
      Zum dem Punkt bzgl. der nativen Funktionen und Namespaces: Hier hat sich meine persönliche Einstellung zu Funktionen und Namespaces durchgesetzt: Ich bin der Meinung, dass Funktionen, die nicht Teil einer Klasse oder eines Objekts sind, auch nicht namespaced sein sollten. Dies ist natürlich in PHP anders umgesetzt, trotzdem widerstrebt mir dieses Prinzip. Von der Funktionsweise her ergibt sich hier kein Unterschied, da intern ein Fallback auf den Root-Namespace passiert. Würdest du dies explizit erledigen um PHP den Fallback zu ersparen?
      Viele Grüße,
      Paul

  2. Der Grundgedanke an Traits ist meiner Meinung nach nicht schlecht. Jedoch finde ich die Umsetzung ziemlich unglücklich.

    – Statt use innerhalb einer Klasse zu verwenden, würde ein Stichwort wie uses viel besser in die Klassendefinition passen (Bsp. class User uses Json\Serializable {…}). Das passt besser zu bestehenden Konzepten wie extends und implements und ist auch einheitlicher. Konflikte kann man dann direkt in der Klasse lösen (ein Bsp. habe ich auf die Schnelle nicht parat).

    – Innerhalb eines Traits abstrakte Methoden definieren zu dürfen, klingt irgendwie auch total verwirrend.

    – …

    Wenn Traits so released werden, wie aktuell implementiert, dann bin ich der Meinung, dass man ein Sprach-Feature auf die PHP-Entwickler los lässt, welches wieder zu schlechtem und unübersichtlichen Code führt. Warum konzentriert man sich nicht auf das Wesentliche und implementiert erstmal eine grundsolide Mehrfachvererbung? Ist das etwa zu „old-skool“? Es bleibt außerdem noch völlig unerwähnt, wieviel Performance-Overhead durch Traits hinzu kommt. Ich bin gespannt, was aus den Traits wird.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert