OOP in PHP – Teil 14: Traits

Traits wurden mit Version 5.4 in PHP eingeführt. Wer sie richtig zu nutzen weiß hat damit ein mächtiges Feature an der Hand um wiederverwendbaren Code zu schreiben. Die Funktionsweise der Traits ist dabei relativ simpel, ein paar Fallstricke gilt es aber zu beachten. Welche das sind erfährst du in diesem Artikel.

Was sind Traits?

In der Programmierung lassen die Namen von Sprachfunktionalitäten ja meist schon darauf schließen, was sie eigentlich tun. Trait können wir mit Merkmal oder Eigenschaft übersetzen. Grob formuliert bieten Traits uns die Möglichkeit, unsere Klassen um bestimmte Merkmale – also Eigenschaften und Methoden – zu erweitern.

Wenn du dich an meinen Artikel zum Thema Vererbung erinnerst denkst du dir jetzt vermutlich:

Wie? Das ist alles? Aber das geht doch mit Vererbung schon ewig. Old News!

Und du hättest sogar recht damit. Eine wichtige Einschränkung bei der Vererbung ist jedoch, dass jede Klasse nur von einer anderen Klasse erben kann. Und genau hier kommen die Traits ins Spiel.

Jede Klasse kann beliebig viele Traits verwenden und im Gegensatz zu Interfaces haben wir in Traits auch die Möglichkeit, vollständige Methoden zu implementieren – nicht nur die Funktionssignaturen. Dadurch können wir einmal geschriebene Funktionalitäten einerseits gut wiederverwenden und andererseits unsere Klassen schnell und einfach mit zentralen Methoden versehen.

Bevor wir auf ein paar Besonderheiten eingehen, schauen wir uns doch erstmal so einen Trait an und wie wir ihn benutzen.

 

 

Hello Trait

So einfach kann es sein. Mit dem Schlüsselwort trait und einem Namen erzeugen wir einen neuen Trait und füllen diesen – genau wie eine Klasse – mit Funktionalität.

Innerhalb einer (oder mehrerer) Klassen verwenden wir den Trait dann mit Hilfe des use Schlüsselworts und dem vergebenen Namen. Mehr ist nicht nötig, in diesem Moment bekommt unsere Klasse alle Funktionalitäten die der Trait mitbringt, wie wir im Beispiel sehen können.

Möchten wir mehrere Traits innerhalb der selben Klasse verwenden können wir die Namen einfach mit Komma trennen. Ungefähr so:

 

 

Du sollst deine Eltern ehren

Dieses Gebot beherzigen Traits leider nicht so ganz. Schauen wir uns folgendes Beispiel an:

Hier wird es schon etwas komplizierter. Wir haben eine Elternklasse, eine Kindklasse und einen Trait – und alle drei implementieren die Methode hello(). Aber welche konkrete Methode wird nun am Ende aufgerufen? Hätten wir keinen Trait wäre die Sache klar – die Methode der Kindklasse überschreibt die Methode der Elternklasse.

Dafür ist es nun wichtig, die Rangfolge der Traits zu kennen – andernfalls bekommen wir es in der Entwicklung mit äußerst unvorhergesehenem Verhalten zu tun. Ich spreche da aus Erfahrung.

Ein Trait überschreibt grundsätzlich gleichnamige Methoden von Elternklassen der Klasse, die ihn verwendet. Wird jedoch eine gleichnamige Methode von der Klasse selbst implementiert überschreibt diese automatisch die Traitmethode. Das lässt sich natürlich umgehen, dazu kommen wir aber später noch.

In unserem obigen Beispiel würde die Ausgabe also Feed me! lauten, da die Klasse MyChild die Methode des Traits überschreibt und diese die Methode der Klasse MyParent überschreibt.

Innerhalb einer Traitmethode haben wir allerdings die Möglichkeit, mit parent:: auf die Methoden der Elternklasse zuzugreifen – genau wie in normalen Klassen.

 

 

Konfliktmanagement

Jetzt haben wir zwar die Rangfolge von Trait- und Klassenmethoden geklärt aber dir als aufmerksamen Leser ist sicherlich etwas aufgefallen:

Ja und was ist, wenn meine Klasse zwei Traits verwendet und beide dieselbe Methode implementieren?

Gut aufgepasst, das ist tatsächlich ein Problem! Zwar kann man dieses Problem natürlich lösen – aber schön ist anders.

Nehmen wir folgende Vorgabe:

Dieses Beispiel wirft einen Fehler, denn der PHP Interpreter weiß nicht wie er reagieren soll. Beide Traits haben dieselbe Rangfolge, implementieren aber dieselbe Methode – welche wollen wir als Programmierer nun haben? Um dieses Problem zu lösen gibt es das Schlüsselwort insteadof. Unsere Klasse müsste also so aussehen:

Damit geben wir der Methode hello() von Trait A den Vorzug über die Methode hello() von Trait B. Jetzt verstehst du sicherlich was ich mit schön ist anders meine.

Trotzdem gibt es auch noch die Möglichkeit, beide Methoden zur Verfügung zu stellen und zwar können wir einzelne Traitmethoden mit dem as Schlüsselwort für diese spezielle Klasse umbenennen.

 

 

Und wozu das Ganze?

An dieser Stelle möchte ich ein praktisches Beispiel bringen, wo ich selbst Traits in Verbindung mit Laravel kürzlich eingesetzt habe. In einem etwas älteren Projekt kam die Anforderung auf, dass manche bestehenden Inhalte nur noch von Usern mit einer bestimmten Berechtigung gesehen und bearbeitet werden können. Die Inhalte der Seite sollten also auf mehrere Usergruppen aufgeteilt werden.

Ohne Traits hätte ich nun an etwa zwanzig oder mehr Stellen Sicherheitsabfragen, WHERE Bedingungen und ähnliches einfügen müssen – ein Albtraum. So konnte ich einen Trait schreiben, den ich von allen betroffenen Models benutzen lasse.

Laravel feuert Events wenn Models auf die Datenbank zugreifen. Innerhalb dieses Traits konnte ich also diese Events implementieren und dafür sorgen, dass jedem SELECT Befehl bei den betroffenen Models die passende WHERE Bedingung hinzugefügt wird. Außerdem überprüfe ich bei INSERT, UPDATE und DELETE einfach ob der User das überhaupt darf und wenn nicht breche ich den Datenbankzugriff ab. So konnte das komplette Thema mit einer minimalen Änderung (nämlich einem use Statement) innerhalb der vorhandenen Klassen vorgenommen werden und ist wesentlich flexibler als jede einzelne Datenbankquery anzufassen.

Dies und Das

Nachdem die vorherigen Teile hoffentlich die grundsätzliche Funktionsweise von Traits klar gemacht haben möchte ich hier noch auf ein paar kleinere Sonderfälle eingehen.

Eigenschaften

Traits können nicht nur Methoden sondern auch Eigenschaften deklarieren. Wichtig ist hierbei, dass die Eigenschaft nicht erneut von der Klasse deklariert werden kann, dies wirft einen Fehler.

 

Statische Methoden

Auch statische Methoden können innerhalb von Traits genau wie in Klassen definiert werden. Diese stehen dann ganz normal über den Klassennamen zur Verfügung.

 

Abstrakt

Neben statischen können wir noch abstrakte Methoden definieren. Dies ist nötig, wenn wir sicherstellen wollen, dass eine Klasse die unseren Trait verwendet, eine Methode implementiert auf die wir zugreifen.

 

 

Aus einem geplanten kurzen Artikel zu einem nützlichen kleinen Feature ist jetzt ein halbes Buch geworden. Aber das ist nicht so schlimm. Ich hoffe, ihr konntet mit den Erklärungen etwas anfangen und verwendet Traits in Zukunft selbst gern. Bei Fragen schreibt mir einfach einen Kommentar unter den Artikel.

9 Kommentare zu “OOP in PHP – Teil 14: Traits

  1. Vielen Dank für die tollen Erklärungen, man merkt hier wurde viel Zeit und Arbeit reingesteckt.
    Eine bessere Erklärung als auf diesen Seiten konnte ich nicht finden, das muss auch mal lobend erwähnt werden.

    Als blutiger Anfänger ist das komplexe Thema OOP leider immer noch sehr hoch für mich. Die umfangreichen
    Ausführungen auf diesen Seiten haben mir jedoch ein wenig die Angst genommen und mir Mut gemacht,
    mich näher damit zu beschäftigen.

    Mir fehlt nach dem lesen und verarbeiten der Informationen noch ein wenig die Fantasie, wie ich all das in einer
    echten Anwendung sinnvoll nutzen kann. Das Potenzial Fehler zu machen scheint auf jeden Fall riesig zu sein.

    • Hi René,

      vielen Dank für das Lob, das freut mich sehr. 🙂

      Mir ging es genauso, ich habe die OOP in „normaler Programmierung“ gelernt wo es auch viel mehr Sinn gemacht hat und mich dann immer gefragt, wo man das nun in der Webprogrammierung einsetzen sollte.
      Aber seit ich mehr mit Frameworks wie zum Beispiel Laravel und Symfony arbeite ist es durchaus sinnvoll: Immer dann, wenn du Code öfter wiederverwenden möchtest. 🙂

      Ein Beispiel (mit dem ich relativ früh angefangen habe) war eine eigene Datenbankklasse – also eine Klasse, mit der ich zentral Queries absenden kann. Und beim Erzeugen der Klasse entscheide ich einfach, an welche Art von Datenbank (MySQL, PostgreSQL etc) das ganze gesendet werden soll.
      Da die Funktionen dafür in PHP immer anders heißen (mysqli_query, pg_query etc) müsste ich sonst bei einem Wechsel überall Funktionen austauschen. Mit einer ordentlich programmierten Klasse ist das nur ein Parameter im Konstruktor (zum Beispiel) der ausgetauscht wird.

      Das Fehlerpotenzial ist da aber aus jedem Fehler lernst du. 😉

      Grüße
      Frank

  2. Also den Unterschied zwischen Klassen und Traits habe ich nicht verstanden.
    Ob ich jetzt eine Klasse oder einen Trait definiere, erscheint mir egal.

    Ich nutze im Hauptprogramm doch beides doch gleich oder wo ist mein Denkfehler?!

    Auch bei Interfaces habe ich Probleme.
    Wenn ich möchte, dass eine Methode implementiert werden MUSS, na dann implementiere ich sie doch einfach direkt in die Klasse.

    Du schreibst, dass der Vorteil von Interfaces ist, dass mehrere Leute an einem Projekt arbeiten können. Aber dann können diese Entwickler doch auch direkt in der Klasse die erforderliche Methode „reinprogrammieren“?!

    • Hi Jan,

      danke für deinen Kommentar.

      Der Unterschied ist folgender: Ein Trait kann für sich allein genommen nicht benutzt werden sondern nur, wenn er mit dem use Keyword in einer Klasse verwendet wird. Du hast dadurch den Vorteil, dieselbe Funktionalität in mehreren Klassen zu verwenden.
      Stell es dir wie eine Gewürzmischung beim Kochen vor. Du kannst sie zwar in mehreren Gerichten benutzen aber für sich allein ist sie einfach ziemlich sinnfrei. 🙂

      Auch bei den Interfaces geht es eigentlich um mehrere Klassen. Bei einer einzigen Klasse bringt ein Interface nicht viel. Es legt eigentlich nur fest „Jede Klasse, die dieses Interface implementiert hat garantiert diese Methoden“. Wenn das Interface aber nur von einer Klasse implementiert wird bringt das natürlich nicht viel.

      Das Schöne im Interface ist aber, dass es die Methoden vorgibt. Stell dir vor eine Klasse implementiert zwei Interfaces – eins mit Methode A und eins mit Methode B. Mitarbeiter Hugo möchte jetzt etwas bauen, das für alle Klassen mit Interface A geeignet ist. Wenn du kein Interface definiert hast sondern einfach nur in allen Klassen dieselben Methoden einbaust müsste er mühsam alle Klassen durchschauen, welche Methoden jetzt für diese Funktionalität wichtig sind.

      Ich hoffe, das hat es etwas klarer gemacht, sonst schreib einfach nochmal. 🙂

      Viele Grüße
      Frank

  3. Hallo 🖐

    Ich programmiere nun seit vielen Jahren und muss sagen, dass ich einige OOP Sachen so selten gebrauche, dass sie mir irgendwie noch immer ein Rätsel waren (man könnte auch sagen, ich habe mich darum gedrückt ;).

    Ich habe daß hier die letzten Tage nachgeholt und aufgefrischt und apropos frisch, ich mag Deinen für Entwickler seltenen und daher auffällig frischen Schreibstil. Wirklich angenehm zu lesen, hat richtig Spaß gemacht und die Lerneinheiten haben genau richtige Häppchen Größe. Man isst aus Appetit einfach weiter und weiter bis man erstaunt feststellt, dass das Tablett bereits leer ist.👌

    Vielen lieben Dank dafür ✌

    • Hi Timm,

      vielen Dank, freut mich sehr zu hören 🙂

      Und das mit den OOP Sachen geht mir ganz genau so. Ich lerne immernoch jeden Tag neue Dinge dazu bei denen ich mir denke „Das hätte ich echt mal von Anfang an gebraucht“ 😉

      Grüße
      Frank

      • Hallo Frank,

        ich hoffe Du teilst auch dieses Wissen wieder mit uns :-)!
        Wie Du wahrscheinlich bemerkst, bin ich aktuell wieder einmal hier und „frische nach“ 😉

        Ich habe mir die letzten Tage einen 60 Fragen schweren „PHP OOP Test“ entworfen, den ich nun abschließend noch in Symfony umsetzen möchte, da auch hier mein letzter Stand Symfony 2 vor x Jahren war…

        Du siehst, Deine Arbeit trägt Früchte 😉

        Ich freue mich auch hoffentlich baldige Updates :)!

        Viele Grüße.
        Timm

Schreibe einen Kommentar

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

*