Was sind eigentlich Traits?

Mein Urlaub ist vorbei und ich hatte ein wenig Zeit mal einen Blick auf Traits zu werfen. Seit PHP 5.4 gibt es nun diese ominösen Traits. Dieses sagenumwobene Konstrukt, welches ich lediglich aus der PHP Dokumentation kannte und für nicht wichtig befunden habe. Zugegeben war dies eine maßlose Unterschätzung von Traits, denn sie können in objektorientierten PHP Umgebungen wirklich für flache Hierarchien sorgen und einiges vereinfachen. Ich werde in diesem Artikel mal kurz beschreiben, was Traits sind und wie man diese praktisch anwenden kann.

Was sind Traits?

Zugegeben, die Beschreibung in der PHP Dokumentation ist eher irreführend als verständlich. PHP Traits sind mit meinem eigenen Worten beschrieben einfach Code, den man wiederverwenden kann. Quasi die PHP Recyclingtonne. Das mag jetzt erstmal komisch klingen, ergibt für PHP aber enormen Sinn. Eine PHP Klasse kann nur von einer einzigen Klasse erweitert werden. Man spricht hier von einfacher Vererbung. Was passiert aber, wenn ich weitere Funktionalitäten nutzen möchte? Bisher habe ich dann die entsprechenden Eigenschaften und Funktionen in die Klassen geschrieben, die sie benötigt haben. Wurden diese Eigenschaften und Methoden von mehreren Klassen benötigt, wurden diese eben in die Elternklasse geschrieben, auch wenn gar nicht alle abgeleiteten Klassen diese Eigenschaften und Funktionen benötigten. So wirklich sauber war das bisher nicht. Glücklicherweise gibt es seit geraumer Zeit aber Traits.

Traits sind im Grunde genommen wie abstrakte Klassen. Man kann sie nicht direkt initialisieren und sie können Eigenschaften und Methoden enthalten, die direkt in den Klassen angewendet werden können, die dieses Trait dann verwenden. Traits sind eine sinnvolle Erweiterung der einfachen Vererbung unter PHP.

Das praktische Beispiel

Für einen meiner Kunden ging es in einem umfangreichen Projekt um die Filterung von Informationen. Man benötigte immer nur bestimmte Informationen aus einem Array unter Anwendung der FilterIterator Klasse. Zunächst das Beispiel ohne Traits.

declare(strict_types=1);
namespace MMNewmedia\Filter;

class NameFilterIterator extends \FilterIterator {
    protected $name;

    public function __construct(\Iterator $oIterator, string $sName) {
        parent::__construct($oIterator);
        $this->name = $sName;
    }

    public function accept() : bool {
        $aCurrent = $this->getInnterIterator()->current();
        if ($aCurrent['name'] === $this->name) {
            return true;
        }

        return false;
    }
}

 
Dieses Code Beispiel stellt eine einfache Filter Iterator Klasse dar, welche nach einem Namen in einem Array sucht. Hier noch kurz der Code, wie man diesen Filter Iterator anwendet.

use MMNewmedia\Filter\NameFilterIterator;

$aDaten = [
    [
        'name' => 'Marcel',
    ],
    [
        'name' => 'Peter',
    ],
    [
        'name' => 'Martin',
    ],
    [
        'name' => 'Nicolai',
    ],
];

// Nach Marcel suchen
$oFilterIterator = new NameFilterIterator(
    new ArrayIterator($aDaten),
    'Marcel'
);
foreach ($oFilterIterator as $aName) {
    var_dump($aName); // [ 'name' => 'Marcel' ]
}

// Nach Peter suchen
$oFilterIterator = new NameFilterIterator(
    new ArrayIterator($aDaten),
    'Peter'
);
foreach ($oFilterIterator as $aName) {
    var_dump($aName); // [ 'name' => 'Peter' ]
}

 
Was passiert jetzt eigentlich, wenn wir nach Peter suchen? Richtig. Ich muss eine neue Instanz des NameFilterIterators erzeugen. Mit einem Trait kann ich die ganze Sache vereinfachen. Verändern wir also mal unsere NameFilterIterator Klasse ein wenig.

declare(strict_types=1);
namespace MMNewmedia\Filter;

// Zunächst unser Trait
trait FilterIteratorTrait {
    protected $argument;

    public function setArgument($value) : \Iterator {
        $this->argument = $value;
        return $this;
    }

    public function getArgument() 
    {
        return $this->argument;
    }
}

// Die NameFilterIterator Klasse mit Trait
class NameFilterIterator extends \FilterIterator {
    use FilterIteratorTrait;

    public function accept() : bool {
        $aCurrent = $this->getInnterIterator()->current();
        if ($aCurrent['name'] === $this->getArgument()) {
            return true;
        }

        return false;
    }
}

 
Das Trait ist eigentlich ziemlich simpel. Es beinhaltet eine Eigenschaft, die unser Suchargument (in diesem Fall der Name) darstellt. Weiterhin enthält das Trait die entsprechenden get und set Methoden, um das Suchargument zu erhalten oder zu setzen. Warum ich das so gemacht habe, und welche Vorteile diese Vorangehensweise bietet, zeige ich gleich in einem praktischen Beispiel. Unsere NameFilterIterator Klasse benutzt dieses Trait nun mittels eines simplen use Statements. Zudem hat sich die FilterIterator Klasse vereinfacht. Wir können komplett auf den Konstruktor verzichten. Da wir das Trait einbinden, können wir die getArgument() Methode des Traits in der Klasse benutzen.

Wie sieht die Anwendung nun aus? Gehen wir einfach von unserem zweidimensionalen Array aus. Schaut einfach selbst.

// Filter Iterator initialisieren
$oFilterIterator = new NameFilterIterator(new ArrayIterator($aDaten));

// Nach Marcel suchen
$oFilterIterator->setArgument('Marcel');
foreach ($oFilterIterator as $aName) {
    var_dump($aName); // [ 'name' => 'Marcel' ]
}

// Nach Peter suchen
$oFilterIterator->setArgument('Peter')->rewind();
foreach ($oFilterIterator as $aName) {
    var_dump($aName); // [ 'name' => 'Peter' ]
}

 
Einfach, oder? Ich benötige nur noch eine Filter Iterator Instanz und kann das Suchargument einfach während der Laufzeit ändern. Die Position des Iterators wird dann einfach wieder zurück gesetzt und schon kann die Suche von vorn losgehen. Die Speicherauslastung ist minimaler, als bei der Verwendung von mehreren Instanzen. Zudem kann ich das Trait auch in weiteren Filter Iteratoren nutzen, bei denen ich mehrere Argumente zur Suche verwenden muss. Die Hierarchie bleibt einfach flach, weil ich nur von der FilterIterator Klasse einfach vererbe, ohne noch mal eine abstrakte Klasse dazwischen schieben zu müssen, die die Methoden des Traits enthält.
 

Die Vorteile von Traits noch mal zusammengefasst

Also fassen wir mal zusammen …

  • Traits können frei verwendet werden und sind wiederverwendbar. Unser Trait aus dem Beispiel kann auch in weiteren Filter Iteratoren verwendet werden.
  • Die einfache Vererbung wird durch Traits erweitert. Da ich mit PHP nicht mehr als eine Klasse als extend angeben kann, sind Traits eine sinnvolle Erweiterung der Vererbung.
  • Die Komplexität von vererbten Klassen wird reduziert. Vor PHP 5.4 hätte ich eine abstrakte Klasse zwischen NameFilterIterator und FilterIterator schieben müssen, die die Methoden des Traits enthalten hätte.

Unfassbar, dass ich Traits so lang missachtet habe. Wenn man erstmal erkannt hat, wie sinnvoll Traits eigentlich sind, ergeben sich daraus einige nützliche Anwendungsfälle. Alle größeren Frameworks, wie Symfony und Zend Framework arbeiten ebenfalls seit geraumer Zeit mit Traits. Daraus ergeben sich in vielen Fällen auch einfach Geschwindigkeitsvorteile. Viel größer ist aber der Effekt der Wiederverwendbarkeit und der flachen Strukturen.

Welche Anwendungsfälle für Traits fallen Euch noch ein oder benutzt ihr bereits Traits in konkreten Projekten?

5 Gedanken zu „Was sind eigentlich Traits?“

  1. Super Post! Ich entwickle gerade ein etwas größeres objektorientiertes WordPress-Theme und war auf der suche nach einer einfachen und sauberen Möglichkeit PHP Code in vielen klassen wiederzuverwenden. Ich habe einige Anwendungsfälle für Traits.

    Deine Erklärung hat mir sehr viel mehr geholfen als die PHP Dokumentation

    Antworten
  2. Naja, den eigentlichen Vorteil demonstriert das Beispiel nicht. Der kommt ja erst bei mehreren Ableitungen zum Vorschein. Das oben im Trait befindliche hätte man auch direkt als Methoden in die NameFilterIterator-Klasse schreiben können. Erst ein Beispiel mit zwei FilterIteratoren würde den Vorteil aufzeigen.

    Antworten

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.