Objektorientierte Programmierung


Du besitzt PHP-Grundkenntnisse, hast um OOP aber bislang einen Bogen gemacht? Dann richtet sich dieses Tutorial genau an dich. Es wird das Thema OOP nicht umfassend abhandeln, aber dir ein solides Fundament vermitteln, mit dem du in die OOP-Welt starten kannst. PHP-Grundkenntnisse werden zwingend vorausgesetzt, allerdings auch Ausschnittsweise noch einmal wiederholt.
Meine Motivation dieses Tutorial zu schreiben ist, dass ich glaube, dass PHP-Einsteiger eigentlich sehr früh an OOP herangeführt werden sollten, und das auch gar nicht schwer ist. Denn wenn das nicht geschieht, verzichtet man als Neuling zu lange auf OOP und verbaut sich damit den Weg zu gutem Code.
Damit das nicht zu schwierig wird, werde ich manchmal Sachverhalte vereinfacht darstellen und einige Probleme einfach auslassen. Ich werde auch manchmal etwas ungenau sein, aber ich glaube, dass es an diesen Stellen besser ist, auf Detailinformationen zu verzichten. Also, legen wir los.

Was ist OOP?


Objektorientierte Programmierung ist ein fundamentaler Programmierstil. Ganz einfach ausgedrückt bedeutet es, dass es nun plötzlich sogenannte Objekte gibt und man mit diesen arbeitet. Am einfachsten kann man sich darunter etwas vorstellen, wenn man sie als Abbildung von Realwelt-Objekten sieht: Ein Auto, ein Haus, eine Person. Objekte besitzen typischerweise Eigenschaften und man kann irgend etwas mit ihnen tun.
Konkret ist ein Objekt zunächst einfach mal eine Variable. Allerdings nicht vom Datentyp Integer oder String, sondern sie ist eben ein Objekt. Hier, da läuft gerade eins vorbei:
$object

Putzig und ganz harmlos das kleine, oder? Beißt garantiert nicht.

Warum dem OOP-Pfad folgen?


OOP gilt in der PHP-Welt als guter und zeitgemäßer Programmierstil. Für Anfänger sind die Vorteile von OOP sicher nicht immer ganz leicht einsichtig, daher werde ich das Thema hier nicht vertiefen.
Viele Argumente, die man oft im Zusammenhang mit OOP hört, sind nicht ausschließlich OOP-Vorteile und können auch anders erreicht werden. OOP hat sich aber durchgesetzt.

Das erste Objekt


Nun zur Tat: Das erste Objekt muss her! Hier der einfachste Code, der mir dazu einfällt:
class AKlasse {

}

$meinAuto = new AKlasse;

Geschockt? Dann erst mal tief durchatmen. Bereit zum Weitermachen?
Dir ist sicher das Keyword class aufgefallen. Damit wir ein Objekt erstellen können, müssen wir dafür zunächst eine Klasse erstellen. Auch das Konzept der Klasse lässt sich in der Realwelt vorfinden: Der Flugzeugträger USS John F. Kennedy ist ein Schiff der Kitty Hawk-Klasse. Genau wie sein Schwesterschiff, die USS Constellation.
Oder nehmen wir einfach die A-Klasse eines bekannten Automobilherstellers. Alle Fahrzeuge, die eine A-Klasse sind, werden nach dem selben Bauplan erstellt. Und genau das ist auch die Bedeutung einer Klasse in PHP: Sie ist ein Bauplan für die Erstellung eines Objekts.
Indem wir in unserem Beispiel die Klasse "Auto" angelegt haben teilen wir PHP mit, wie unser Objekt $meinAuto zu erstellen ist. In unserem Beispiel erzeugen wir also ein Objekt namens $meinAuto der Klasse AKlasse. Üblicher Weise schreibt man Klassennamen mit Großbuchstaben am Beginn, Objektnamen (wie alle Variablennamen), mit einem Kleinbuchstaben. Somit kann man eine Klasse namens "Auto" und ein Objekt "$auto" deklarieren, ohne dass es zu Missverständnissen kommen kann.
Unsere Klasse ist komplett leer, unser Auto kann also bislang nichts. Beweis gefällig? Wenn wir uns mit
var_dump($meinAuto);

die Eigenschaften des Autos anzeigen lassen, wird uns nur folgendes ausgegeben:
object(Auto)#1 (0) { }

In den geschweiften Klammern stehen die Eigenschaften. Oder stünden, wir haben ja keine festgelegt. Immerhin, PHP weiß offensichtlich, dass $meinAuto eine AKlasse ist.
Objekte erzeugen wir immer mit Hilfe des Keywords new. Mit new weisen wir PHP also an, aus unserem Bauplan wirklich ein konkretes Objekt zu bauen. Ein Objekt einer Klasse nennt man auch Instanz einer Klasse.

Eigenschaften und Methoden


Nun gut, unser Auto ist reichlich langweilig. Es braucht dringend ein paar Eigenschaften, beispielsweise eine Farbe und das Baujahr. Was ich bislang immer Eigenschaften genannt habe, sind in PHP Variablen, die zu einem Objekt gehören. Anstatt also irgendwo eine Variable $farbe und $baujahr zu deklarieren, können wir die beiden in die Klasse schreiben:

class AKlasse {
public $farbe = 'rot';
public $baujahr;
}

Die Klasse sagt uns nun, dass neue Auto-Objekte mit den Variablen $farbe und $baujahr gebaut werden (man nennt das auch instanziieren, weil beim Bauen eines Objekts eine Instanz erzeugt wird). Die Farbe aller neuen Autos ist immer rot. Ein Baujahr haben unsere Autos aber nicht vorgegeben. Ist ja auch klar, wenn wir
public $baujahr = 2014;

schreiben würden, dann wäre das Baujahr immer 2014, egal, in welchem Jahr wir uns in Wirklichkeit befinden. Daher werden wir uns später selber darum kümmern, einen Wert für $baujahr festzulegen. Da wir keine Angabe gemacht haben, ist der Wert von $baujahr null. Beweis mit var_dump($meinAuto):
object(Auto)#1 (2) { => string(3) "rot" => NULL }

Dann wäre da noch eine Sache: Was soll dieses "public"? Wir müssen für Objekte festlegen, wie ihre Variablen sichtbar sein sollen. Es gibt drei Möglichkeiten: public, protected und private. Zu protected kommen wir noch, aber public heißt öffentlich und private heißt privat. Ja wirklich! Das bedeutet, dass wir auf eine als public deklarierte Variable von außerhalb der Klasse zugreifen können, auf eine als privat deklarierte aber nicht. Greifen wir einmal zu:
$meinAuto = new AKlasse;
echo $meinAuto->farbe;

Die Ausgabe ist wenig überraschend "rot". Wenn wir aber public für die Variable $farbe auf private ändern, dann wird PHP ungehalten (error reporting muss dazu aktiviert sein): "Fatal error: Cannot access private property AKlasse::$farbe"
Wird deklarieren deshalb immer alle Objektvariablen als public. Weil wir keinen guten Code schreiben möchten. Oder wird deklarieren nur diejenigen Variablen als public, bei denen es egal ist, was damit passiert. Zum Beispiel, weil uns egal ist, ob irgend jemand mal die Farbe unseres Autos einfach auf 'grün' ändert. Das ist dann relevant, wenn eine Variable bestimme Bedingungen einhalten soll. Bei unserer Variablen $baujahr ist das der Fall: Es sollte vorzugsweise nicht Werte wie 123 oder "Schrödingers Katze" annehmen, denn im Jahr 123 gab es bekanntlich noch keine Autos und was Katzen mit Baujahren zu tun haben ist erst recht unklar.
Innerhalb unserer Klasse können wir schon dafür sorgen, dass das nicht passiert, aber vertrauen wir auch dem Code außerhalb unserer Klasse? Naja vielleicht, solange das unser Code ist, aber was passiert, wenn wir unsere Klasse als Open Source veröffentlichen und fremde Personen sie benutzen? Wer garantiert uns, dass die immer über das Baujahr nachdenken? Auf dieses Problem kommen wir später noch einmal zu sprechen.
Ganz nebenbei haben wir das erste mal auf die Variable eines Objekts zugegriffen. Das geschieht, in dem wir den Variablennamen des Objekts schreiben, gefolgt von einem Pfeil ("->", der sogenannte Pfeiloperator), gefolgt vom Namen der Variablen. Natürlich können wir so auch schreiben und unserem Auto endlich ein Baujahr zuweisen:
$meinAuto->baujahr = 1969; // ein sehr altes Auto

Wichtig ist, dass wir dabei den Variablenname ohne Dollarzeichen schreiben müssen. PHP und dessen Syntax sind leider nicht immer beste Freunde. Zwar gibt es für alle Absurditäten einen guten Grund, das lässt sie aber leider auch nicht verschwinden.
Die Erklärung hierfür nur ganz kurz und am Rande: Wenn wir $meinAuto->$baujahr schreiben, dann wird $baujahr nicht als Variable des Objekts aufgefasst, sondern als davon unabhängige Variable. $baujahr ist also aus Sicht von PHP einfach eine Variable, die nicht im Kontext des Objekts liegt. Wozu dieses Verhalten wiederum gut ist, würde dich mit zu großer Wahrscheinlichkeit nur vom Kernthema ablenken (Stichwort: Variabler Objektvariablenzugriff - alles klar?). Unbedingt merken: Das Dollarzeichen lässt man beim direkten Zugriff auf Objektvariablen weg.
Nun können wir also Klassen anlegen und dort Variablen deklarieren. Schauen wir in die reale Welt, dann haben unsere Objekte aber nicht nur Eigenschaften, sie können oft auch etwas tun. Das Auto kann zum Beispiel fahren. Für PHP heißt das, dass wir unsere Klassen auch mit Code ausrüsten können. Dazu übernehmen Klassen das Konzept der Funktionen. Wir können also Funktionen in Klassen einbauen, dort nennt man sie dann Methoden.
Wir haben vorhin schon festgestellt, dass es besser wäre, $baujahr als private zu deklarieren. Das machen wir nun. Somit kann von außerhalb des Auto-Objektes kein unmöglicher Wert eingetragen werden, denn von außen dürfen wir $baujahr nun gar nicht mehr ändern. Damit wir aber doch ein Baujahr festlegen können, fügen wir außerdem eine Methode hinzu, die ein paar Gültigkeitsüberprüfungen macht.
class AKlasse { 
public $farbe = 'rot';
private $baujahr;

public function setBaujahr($baujahr)
{
$baujahr = (int) $baujahr;
if ($baujahr >= 1886) {
$this->baujahr = $baujahr;
}
}
}

Wir benutzen das bekannte Keyword function, um eine Methode zu deklarieren. Auch hier müssen wir wieder die Sichtbarkeit festlegen. public ist die richtige Wahl: Wir wollen ja, dass mit dieser Methode von außen das Baujahr festgelegt werden kann. Wie von Funktionen bekannt können außerdem Parameter übergeben werden, hier $baujahr.
Die Methode wandelt $baujahr vorsorglich in einen Integer um ("Schrödingers Katze" wäre damit dann erledigt und eindeutig tot) und speichert das Jahr nur dann, wenn es nach der Erfindung des Automobils liegt. Für unsere Autos ist damit garantiert, dass das Baujahr immer halbwegs plausibel ist, sofern denn ein Wert gesetzt wurde (wir erinnern uns: $baujahr ist zunächst null).
Mit dem Keyword $this bezeichnen wir das Objekt, in dem wir uns gerade befinden. Das können wir natürlich nur innerhalb von Klassen (genauer Klassenmethoden) schreiben. Wichtig ist aber folgendes: $this meint nicht die Klasse, in der es sich befindet, sondern das Objekt! Diese Unterscheidung ist sehr wichtig. Ein Objekt und seine Klasse sind nicht das selbe. Wir erinnern uns: Eine Klasse ist nur der Bauplan für ein Objekt.
$this->$baujahr erlaubt uns also innerhalb des Objekts den Zugriff auf die Variable $baujahr. Damit ist es dann auch kein Problem, dass in der Methode zwei Variablen mit dem Namen $baujahr vorkommen - sie lassen sich unterscheiden. Man muss $this angeben und kann nicht nur zum Beispiel $baujahr = 1969 schreiben. In anderen Programmiersprachen wie JAVA geht das: JAVA erkennt, dass mit $baujahr eigentlich $this->baujahr gemeint ist, wenn keine lokale Variable mit gleichem Namen existiert. PHP hingegen legt statt dessen eine neue lokale Variable $baujahr an.
Wie aber rufen wir unsere Methode denn nun auf, um unserem Auto ein Baujahr zu verpassen? Das geht wie folgt:
$meinAuto->setBaujahr(1969);

Natürlich könnten wir diesen Methodenaufruf auch innerhalb unserer Klasse schreiben. Das sähe dann so aus:
$this->setBaujahr(1969);

Hier kommt dann wieder $this zum Einsatz. Wenn wir das innerhalb der Methode setBaujahr() schreiben, basteln wir uns eine wunderschöne Endlosschleife, da sich die Methode immer wieder selber aufruft. Aber wir könnten uns natürlich auch beliebig viele andere Methoden erstellen und den Aufruf von dort aus starten.
Unsere Methode setBaujahr() nennt man einen Setter. Ein Setter ist eine Methode, die dazu dient, eine Objektvariable zu setzen (speichern). Das ist tatsächlich so simpel, wie es klingt. Ein Setter hat immer genau einen Parameter. Heißt mein Setter setBaujahr(), dann ist der Parameter $baujahr. Hieße der Setter setFarbe, dann wäre der Parameter $farbe und er würde in der Objektvariablen $farbe gespeichert.
Im Gegenzug gibt es dann auch Getter. Getter sind Methoden, die den Wert einer Objektvariablen zurückgeben. Für $baujahr sähe das so aus:
public function getBaujahr($jahr)
{
return $this->$baujahr;
}

So, jetzt ist es soweit. Zurücklehnen. Entspannen. Du hast die wichtigsten Schritte auf dem Weg zum OOP-Jedi bereits hinter dich gebracht. Du bist ein vielversprechender Padawan und hast dein erstes Lichtschwert zusammengebaut. Du kennst Klassen und Objekte sowie die zwei wichtigsten Teile von Objekten: Variablen und Methoden. Wenn du bis hierher inhaltlich folgen konntest, dann schaffst du mit Sicherheit auch den Rest deiner Ausbildung zum OOP-Jedi. Denn das meiste, was jetzt noch folgt, sind lediglich Varianten oder Erweiterungen von dem, was du bereits gelernt hast.

Magische Methoden


Noch ein letztes Mal: Der Sinn, eine Sichtbarkeit festzulegen, ist der, den Zugriff auf Variablen oder Methoden von außen verhindern zu können, damit keine unerwünschten Werte gespeichert oder Aktionen durchgeführt werden können. Bei unserem Auto-Beispiel ist es wenn wir ehrlich sind egal, was in $baujahr steht, aber würde man mit $baujahr nun Berechnungen ausführen, so wäre immer garantiert, dass sich ein Integer darin befindet.
Moment, das stimmt natürlich nicht, $baujahr kann ja auch null sein. Das ist ungünstig. Besser wäre es, wenn wir absolut garantieren könnten, dass $baujahr immer einen gültiger Wert besitzt. Wie wäre es, wenn wir erzwingen, dass beim Bauen (Erstellen/Instanziieren) eines Objektes das Baujahr festgelegt werden muss? Dazu erweitern wir unsere Klasse um eine besondere Methode:
public function __construct($baujahr)
{
$this->setBaujahr($baujahr);
if ($this->baujahr == null) {
throw new Exception('Ungültiges Baujahr!');
}
}

Die Methode nimmt einen Parameter $baujahr und ruft damit die Methode setBaujahr auf. Wir erinnern uns: Die speichert $baujahr in unserer Objektvariablen $baujahr - aber nur, wenn das Baujahr gültig ist. Das prüfen wir dann in der If-Abfrage auch ab: Ist es null, so wurde offensichtlich nichts gespeichert, und wir werfen eine Ausnahme (wir erzeugen künstlich einen Fehler).
Diese Methode nennt man einen Konstruktor einer Klasse (es kann pro Klasse mehrere geben, aber in der Regel nutzt man nur einen - womöglich wirst du niemals mehr als einen einsetzen). Er ist eine besondere Methode, das erkennt man daran, dass sein Name mit "__" (zwei Unterstrichen) beginnt. Solche Methoden sind so besonders, dass man sie magische Methoden nennt.
Das heißt, wenn man eine Methode namens "__construct" erstellt, dann ist das immer eine Konstruktor-Methode, der PHP eine besondere Bedeutung beimisst. Alle magischen Methoden haben besondere Bedeutungen. Sie werden in erster Linie nicht von uns aufgerufen, indem wir zum Beispiel $meinAuto->__construct(1969) schreiben würden, sondern durch PHP selbst. Der Konstruktor wird nämlich beim Konstruieren (Bauen / Instanziieren - viele Wörter für ein und das selbe) des Objekts aufgerufen.
Der Beweis: Wenn wir unseren Code jetzt von PHP ausführen lassen, erhalten wir eine Fehlermeldung: "Warning: Missing argument 1 for Auto::__construct()" PHP vermisst den Parameter für die Konstruktor-Methode. Einerseits logisch, da wir nirgendwo einen solchen Parameter übergeben haben, aber andererseits rufen wir die Methode ja auch nirgendwo auf?
Doch, nur eben indirekt: Wenn wir mit new ein Objekt erstellen, ruft PHP die Konstruktor-Methode auf, sofern sie denn deklariert wurde. Bei uns ist das der Fall. Also sind wir jetzt immer gezwungen, das Baujahr als Parameter zu übergeben, und das geht so:
$meinAuto = new Auto(1969);

Damit ist sichergestellt, dass $baujahr immer eine gültige Zahl und immer gesetzt ist.
Es gibt verschiedene magische Methoden, aber der Konstruktor ist die mit Abstand wichtigste. Wenn du mehr über magische Methoden erfahren möchtest - __toString() ist aus meiner Sicht die zweitwichtigste - dann empfiehlt sich ein Blick in die PHP-Dokumentation. Ich werde nicht weiter auf sie eingehen.

Konstanten


Ich habe dich angelogen. Es gibt keinen Kuchen. Und Objekte bestehen nicht nur aus Variablen und Methoden. Im Rahmen der Didaktik heiligt der Zweck die Mittel, daher darf ich das! Der Einfachheit halber habe ich zwei Aspekte bisher ignoriert: Konstanten und alles Statische.
Genau wie Variablen und Funktionen ist auch das Konzept der Konstanten von Klassen adaptierbar. Auch Klassen können eigene Konstanten besitzen. Das gestaltet sich analog zu Variablen, daher direkt ein Beispiel:
class AKlasse {
const spritverbrauch = 'zu viel';
}

Mit dem Keyword const deklarieren wir eine Konstante. Anders als bei Variablen müssen wir einen Wert angeben. Das ist auch naheliegend, denn wie der Name schon sagt, können wir Konstanten nicht verändern. Eine Konstante ohne anfängliche Wertzuweisung wäre demnach doch reichlich sinnfrei, oder? (Wir können allerdings durchaus explizit null zuweisen, aber das nur am Rande.)
Für Konstanten kann man außerdem keine Sichtbarkeit deklarieren. Sie sind immer public. Auch das ist naheliegend: Da eine Konstante nicht verändert werden kann, ist es nicht möglich, sie von außen - nun ja - zu verändern. Wie wir das von Konstanten außerhalb von Klassen kennen, beginnen sie nicht mit einem Dollarzeichen.
Doch wie greifen wir nun auf eine Konstante zu? $meinAuto->spritverbrauch klappt nicht. Zwar ist die Konstante von außerhalb der Klasse zugreifbar, aber wenn wir das so schreiben, interpretiert PHP das als Zugriff auf eine Variable! Hier kommt der nächste Härtetest für OOP-Anfänger, denn es wird syntaktisch richtig ungemütlich. Man greift so zu:
AKlasse::spritverbrauch

Den doppelten Doppelpunkt nennt man auch Gültigkeitsbereichsoperator. Wenn dir das nicht gefällt, dann kannst du ihn auch Paamayim Nekudotayim nennen. Das ist leider kein Witz. Manchmal möchte PHP von dir, dass du es hasst. Nennen wir ihn also lieber doch Gültigkeitsbereichsoperator oder wir nennen ihn gar nicht mehr beim Namen. Deal?
Damit du es als Anfänger so richtig schwer hast und den Hass ins unermessliche steigerst (ja, PHP steht auf der dunklen Seite der Macht), erlaubt es dir PHP außerdem generell nicht, auf Klassenkonstanten über Objekte zuzugreifen. Das hier geht also nicht:
$meinAuto::spritverbrauch

Eben sowenig natürlich innerhalb von Klassen:
$this::spritverbrauch

Dort muss man entweder ebenfalls den Klassennamen nutzen, oder aber das Keyword self, dass für die Klasse (nicht das Objekt!) steht:
self::spritverbrauch

Anders als $this beginnt self nicht mit einem Dollarzeichen. Ich erwarte nicht von dir, dass du $this und self sowie -> und :: ab jetzt unfallfrei auseinander hältst. Diese Keyword- und Syntaxeigenarten sind alles andere als einsteigerfreundlich. Sie existieren nicht grundlos, aber sie machen es Einsteigern schwer und einige Programmiersprachen gehen durchaus anders damit um.
Beispielsweise ist keineswegs ein Grundsatz von OOP, dass man auf Konstanten nicht über Objekte, sondern nur über Klassen zugreifen darf - PHP könnte das sehr wohl auch erlauben. Es gibt Argumente dagegen, aber Einsteiger, die OOP noch nicht von anderen Programmiersprachen her kennen, interessiert das verständlicher Weise wenig mit Tendenz zu gar nicht. Als schwacher Trost sei gesagt, dass das Schwierigkeitsniveau nicht weiter steigt.
Noch einmal zusammenfassend: $this kann nur innerhalb von Klassenmethoden geschrieben werden und bezieht sich dann auf das aktuelle Objekt. Auch self kann nur innerhalb von Klassenmethoden geschrieben werden, bezieht sich dann aber auf die Klasse.
Das mag schwer nachzuvollziehen sein (warum muss man dies trennen?), ist aber notwendig und wenn man sich die Definition einer Klasse vor Augen führt, auch plausibel: Eine Klasse ist ein Bauplan. Ein Auto-Bauplan kann ja durchaus Informationen (hier Konstanten) enthalten, über die ein fertig gebautes Auto nicht direkt verfügt. Man kann vom konkreten Auto schon den Weg zurück zum Bauplan (der Klasse) finden und die Informationen dort nachschlagen, aber man muss schon eine Unterscheidung treffen. Zwar kann man Abkürzungen zu diesen Informationen einbauen, PHP hat sich aber dagegen entschieden.

Statische Methoden


Wir haben eben gelernt, dass Konstanten nur Klassen zugeordnet werden, Variablen und Methoden aber Objekten. PHP geht uns mächtig auf die Nerven und warum müssen wir uns das eigentlich gefallen lassen? Wie wäre es, wenn wir Variablen und Methoden einfach mal ganz dreist den Klassen zuordnen? Ja jetzt sagst du nicht mehr PHP, jetzt fühlst du dich nicht mehr so stark!
Leider doch. Denn genau das hat PHP ohnehin schon vorgesehen. Schauen wir mal auf folgende Methode:
public function kompletterName($vorname, $nachname)
{
$name = vorname.' '.$nachname;
return $name;
}

Diese Beispielmethode fügt einen Vornamen und einen Nachnamen zu einem vollständigem Namen zusammen. Das hat keinen tieferen Sinn. Wichtig ist nur, dass diese Methode ausschließlich mit lokalen Variablen operiert. Sie benutzt keine Objektvariablen (oder siehst du irgendwo ein $this?). Daher muss diese Methode auch nicht zwingend einem Objekt zugeordnet werden.
Um sie von konkreten Objekten zu lösen, fügen wir das Keyword static ein:
public static function kompletterName($vorname, $nachname)
{
$name = vorname.' '.$nachname;
return $name;
}

Jetzt ist es - ähnlich wie bei den Konstanten - überhaupt nicht mehr erlaubt, die Methode über das Objekt aufzurufen. Angenommen, diese Methode steht in unserer Auto-Klasse, dann würden wir statt
$name = $meinAuto->name('Hurby', 'der nervige Käfer');

dieses schreiben müssen:
$name = AKlasse::name('Hurby', 'der nervige Käfer');

PHP ist konsequent und verlangt auch hier wieder unseren Gültigkeitsbereichsoperator alias den doppelten Doppelpunkt, genau wie bei Konstanten. Man bezeichnet die Methode nun als statische Methode. Würden wir innerhalb der Klasse auf sie referenzieren wollen, könnten wir den Klassennamen oder self verwenden:
$name = self::name('Hurby', 'der nervige Käfer');

Bitte daran denken, dass es nicht möglich ist, innerhalb von statischen Methoden auf Objektvariablen zuzugreifen. Statische Methoden befinden sich auf Klassen- statt auf Objektebene. Daher kann man aus statischen Methoden heraus keine nicht-statischen Methoden aufrufen. Wie sollte das auch gehen, man müsste $this->methodenname() nutzen - aber $this gibt es nicht, da die Methode zu keinem Objekt gehört.

Statische Variablen


Auch Variablen lassen sich der Klasse statt eines Objekts zuordnen. Das ist wahrscheinlich für dich nicht unbedingt intuitiv. Als kleiner Nervenbaldrian sei gesagt: Du wirst in der Praxis den Umgang mit Statischem wahrscheinlich vermeiden oder begrenzen können, wenn du das möchtest. Es zwingt dich niemand eine Methode, die statisch deklariert werden könnte, auch tatsächlich so auszuweisen. In der Tat empfehle ich dir als Anfänger, Methoden nach Möglichkeit nicht statisch zu deklarieren.
Man nutzt statische Variablen in der Regel dann, wenn alle Objekte einer Klasse Zugriff auf eine gemeinsame Variable benötigen. Bauen wir uns dazu ein neues Beispiel:
class AKlasse {
public static $hersteller = 'Mercedes';

public function printHersteller()
{
echo 'Hersteller dieses Autos: '.self::$hersteller;
}
}

class Corvette {
public static $hersteller = 'Chevrolet';

public function printHersteller()
{
echo 'Hersteller dieses Autos: '.self::$hersteller;
}
}

Es gibt nun gleich zwei Klassen. Unser Automobilhersteller hat soeben Konkurrenz erhalten. Alle Autos, die eine A-Klasse sind, haben den selben Hersteller, ebenso alle Autos, die eine Corvette sind. Daher deklarieren wir die Variable $hersteller als statisch. Wir sparen damit Speicherplatz - die Variable muss nur einmal gespeichert werden statt für jedes Auto einzeln - und es ist leichter, sie zu ändern, da wir sie nicht für jedes Objekt einzeln ändern müssen.
Der Zugriff auf Variablen folgt dem bekannten Muster: Wir nutzen den doppelten Doppelpunkt, um die Zugehörigkeit zu einer Klasse anzugeben. Anders als beim Zugriff auf Objektvariablen müssen wir nun aber das Dollarzeichen mit angeben. Wir wissen inzwischen, warum: Nur so kann PHP statische Variablen von Konstanten unterscheiden. Auch jetzt wieder darf self benutzt werden, um innerhalb einer Klasse auf ihre statischen Variablen zuzugreifen. Statische Variablen können initialisiert werden, das ist aber kein Muss - sie werden dann mit null belegt.
Nun habe ich aber einen Fehler gemacht: Chevrolet ist eigentlich nur eine Marke, daher wäre es wohl besser, General Motors (GM) als Hersteller der Corvette anzugeben. Und zwar für alle Corvettes. Das ist ganz einfach:
Corvette::$hersteller = 'General Motors';

Zum Beweis, dass das geklappt hat:
$auto = new Corvette;
$auto->printHersteller();

Die Ausgabe ist: "Hersteller dieses Autos: General Motors"

Vererbung


So ganz zufrieden bin ich aber immer noch nicht. Schon mal was von DRY gehört? Das steht für "Don't repeat yourself", übersetzt also "wiederhole dich nicht". Was das meint wird sofort klar, wenn wir uns die Methode printHersteller() beider Klassen anschauen: Sie sind exakt gleich. Würden wir Klassen für noch mehr Autos anlegen, müssten wir die Methode immer wieder kopieren. Nicht mit mir!
Abhilfe schafft das letzte fundamentale Konzept der OOP, das wir in diesem Tutorial behandeln werden: Die Vererbung. Klassen können von einer Elternklasse erben: Geld, das gute Aussehen, und so weiter. Alles, was eine Elternklasse kann, kann auch eine Kindklasse. Das heißt, Konstanten, Variablen und Methoden sind für die Kindklasse zugänglich.
Wir können also unsere beiden Auto-Klassen von einer gemeinsamen Elternklasse erben lassen. Wenn diese eine Methode namens printHersteller() hat, dann können ihre Kinder sie benutzen. Wir nennen die Elternklasse Auto.
class Auto {
public function printHersteller()
{
echo 'Hersteller dieses Autos: '.self::$hersteller;
}
}

class AKlasse extends Auto {
public static $hersteller = 'Mercedes';
}

class Corvette extends Auto {
public static $hersteller = 'Chevrolet';
}

$auto = new Corvette;
$auto->printHersteller();

Nun müssen wir die Methode printHersteller() nur noch einmal aufschreiben. Dass die beiden Klassen von der Auto-Klasse erben, sagen wir mit "extends Auto". Ein Objekt der Klasse Corvette ist nun auch gleichzeitig ein Objekt der Klasse Auto. Daher können wir $auto->printHersteller() benutzen, um den Hersteller abzufragen.
Wenn du diesen Code nun ausführen lässt, ist PHP aber anderer Meinung: "Fatal error: Access to undeclared static property: Auto::$hersteller" behauptet es da einfach und verweigert die Arbeit. Das Problem ist, dass wir die Variable $hersteller in den beiden Kindklassen deklariert haben, aber nicht in der Klasse Auto selber. Die Klassen Corvette und AKlasse kennen $hersteller natürlich, aber die Klasse Auto kennt diese Variable nicht! Da die Methode printHersteller() aber in der Auto-Klasse liegt, schaut PHP auch genau in dieser Klasse nach der Variable $hersteller.
Um das Problem zu lösen, gibt es das Keyword static. Wenn wir self::$hersteller in static::$hersteller ändern, dann schaut PHP in der Klasse Corvette nach der Variable $hersteller nach und nicht in der Klasse Auto. Dort gibt es die Variable und der Code funktioniert. Dieses recht zimperliche und faule Verhalten legt PHP zum Glück nur bei statischen Variablen und Funktionen an den Tag. Sind sie nicht statisch, dann kann der Zugriff immer über $this erfolgen.

Mehrfachvererbung


Mehrfachvererbung bedeutet, dass eine Klasse von mehreren "gleichberechtigten" Elternklassen erben kann. Das ist in einigen Programmiersprachen, z. B. C++, möglich. Dort kann eine Klasse sozusagen mehr als eine Mutter haben. PHP gibt sich da eher konservativ und vertritt die Ansicht, das so etwas mal gar nicht geht. Allerdings ist es sehr wohl möglich, dass eine Klasse eine Elternklasse hat, die wiederum selber eine Elternklasse hat.
Zunächst hier nochmal, wie es nicht geht:
class Corvette extends Auto, Fahrzeug { }

Was aber geht ist:
class Auto extends Fahrzeug { }
class Corvette extends Auto { }

Das ist zwar nicht exakt das selbe, jedoch ähnlich. Es gibt aber noch eine andere Möglichkeit, die Beschränkung der Mehrfachvererbung aufzuweichen.

Traits


Traits! Ein Trait, zur Deutsch Charaktereigenschaft, ist in etwa eine Klasse, die nur der Vererbung dient. Die Analogie zur Klasse soll nur der Vereinfachung dienen, genauer ist ein Trait eine Sammlung von Methoden, die in Klassen eingebunden werden kann. Das sieht dann zum Beispiel so aus:
trait ExampleTrait {
public function example()
{
echo 'Hello Trait!';
}
}

class ExampleClass {
use ExampleTrait;
}

Die Beschreibung eines Traits wird mit dem Keyword trait eingeleitet. Innerhalb einer Klasse bindet man einen Trait dann mit use ein. Der Clou daran ist, dass man nun beliebig viele Traits inkludieren kann:
use TraitOne, TraitTwo, TraitThree;

Traits sind in PHP noch recht jung, aber in manchen Situationen sehr angenehm. Der Umgang mit ihnen ist außerdem sehr einfach - man deklariert sie ähnlich wie eine Klasse, fügt Methoden hinzu und bindet sie in beliebige Klassen mit use ein. Ein Trait kann selber nicht instanziiert werden, wir können also in unserem Beispiel keine Objekte vom Typ ExampleTrait erstellen sondern müssen den Trait dazu in eine Klasse einbetten. Es ist sogar möglich, Traits in anderen Traits zu benutzen. Das funktioniert ebenfalls mit use.
Eine Anmerkung darf aber nicht fehlen: Es kann sein, dass zwei Traits Methoden gleichen Namens beinhalten. Wenn dann diese beiden Traits in die selbe Klasse eingebettet werden, führt das zu einem Fehler. Solche Namenskonflikte können mit dem insteadof-Operator aufgelöst werden. Darauf gehe ich nicht näher ein sondern verweise auf die Dokumentation.

Protected


Wir kommen dem Ende dieses Tutorials immer näher. Deshalb wird es erneut Zeit für eine Beichte: Lieber Leser, ich habe dir bislang etwas vorenthalten. Erinnerst du dich an public und private? Da gab es doch noch einen dritten im Bunde!
Genau, das war protected. Auch hier hilft eine Übersetzung weiter: protected bedeutet geschützt. Wird ein Mitglied einer Klasse als protected oder private deklariert, ist es von außen nicht mehr zugreifbar. Dabei ist private aber strenger als protected. Eine Methode oder Variable, die private ist, ist auch vor einer Kindklasse geschützt. Das ist bei protected nicht der Fall. Wozu das nützlich ist, sehen wir in Kürze.

Überschreiben


In Zusammenhang mit Traits haben wir festgestellt, das PHP ein Problem damit hat, wenn Methoden aus unterschiedlichen Traits den selben Namen tragen und diese Traits dann in die selbe Klasse eingebunden werden. Klar, denn PHP weiß dann nicht, wie es diese Methoden unterscheiden soll. Was passiert eigentlich, wenn eine Elternklasse eine Methode besitzt, und eine von ihr erbende Kindklasse eine Methode mit dem selben Namen?
In diesem Fall wird in der Kindklasse die Methode der Kindklasse bevorzugt. Ein Sieg des Egoismus! Aber eines nützlichen. Stellen wir uns vor, wir haben uns gerade von GitHub die weltbeste Auto-Klasse herunter geladen. Sie kann Autos auf alle Arten repräsentieren und verwalten, von denen wir je geträumt haben. Sie hat eine Methode setBaujahr() ähnlich wie in unserer eigenen Auto-Klasse. Aber sie akzeptiert nicht, wenn das Baujahr in der Zukunft liegt. Wir möchten das aber eintragen können, damit wir jetzt schon Autos verwalten können, die erst im nächsten Jahr in die Serienproduktion starten.
Was wir dann tun können ist, die Methode in der Auto-Klasse zu verändern. Wenn dann irgendwann mal eine neuere Version der Auto-Klasse veröffentlicht wird, können wir zwar nicht einfach diese neue Version übernehmen, weil wir ja dann die Änderung verlieren, aber das ist jetzt nicht wichtig. Mit den Problemen von morgen beschäftigen wir uns frühestens übermorgen.
Sei ehrlich, hast du jetzt froh und zustimmend genickt? Das machen wir natürlich nicht so! Code von Dritten modifiziert man nicht. Statt dessen erstellen wir eine eigene Klasse, die wir beispielsweise MeinAuto nennen, die einfach die Methode überschreibt:
class MeinAuto extends Auto {
public function setBaujahr($baujahr) { ... }
}

Schön und gut, aber was wäre, wenn die Auto-Klasse eine Methode setFarbe() anbietet, die eine unzureichende Sicherheitsüberprüfung macht? Wir wollen, dass ein Auto immer eine Farbe hat. Die ursprüngliche Methode lässt aber vielleicht zu, dass man null oder einen leeren String übergibt. Das zu ändern ist nicht weiter schwer. Wir ändern einfach die Methode in der Klasse Auto.
Na gut, darauf fällst du jetzt wohl sicherlich nicht herein: Wir verändern die Klasse Auto nicht. Stattdessen kopieren wir einfach die Methode setFarbe() in unsere Klasse MeinAuto und ändern sie dort, oder? Das nun auch nicht. Erinnerst du dich noch an DRY? Wir möchten uns nicht wiederholen. Warum den Code der Methode kopieren - wenn wir die Methode auch einfach aufrufen können!
class MeinAuto extends Auto {
public function setBaujahr($baujahr) { ... }
public function setFarbe($farbe)
{
if ($farbe == null) {
throw new Exception('Fehler! Keine Farbe angegeben!');
} else {
parent::setFarbe($farbe);
}
}
}

Auch wenn die Methode setFarbe() der Klasse Auto in unserer Klasse MeinAuto überschrieben wird, existiert sie dennoch weiterhin und wir können sie aufrufen. Da $this->setFarbe() die Methode aus MeinAuto meinen würde, müssen wir dazu das Keyword parent benutzen.
Du hast möglicherweise bisher den Eindruck gehabt, der doppelte Doppelpunkt komme nur bei statischen Zugriffen zum Einsatz. Dem ist, wie das Beispiel zeigt, nicht so. Wenn wir den Pfeiloperator ("->") nutzen würden, würde das zu einem Fehler führen, obwohl wir ja auf eine objektbezogene Methode zugreifen würden. Bitte beachte auch, das Konstruktoren ebenfalls überschrieben werden können und dann nur der Konstruktor der Kindklasse aufgerufen wird. Dieser kann jedoch mit parent::__construct() den Konstruktor seiner Elternklasse manuell aufrufen.
Das Überschreiben beschränkt sich nicht nur auf Methoden. Auch Variablen können überschrieben werden. Beim Überschreiben von Methoden ist wichtig, dass ihre sogenannte Signatur sich nicht ändern darf: Das heißt, ihre Sichtbarkeit und ihre Parameter müssen denen der Elternklasse entsprechen.
Stichwort Sichtbarkeit: Das Überschreiben ist aber nur möglich, wenn die Variable oder Methode nicht als protected oder final deklariert ist. Wäre sie protected, so wäre ihr Zugriff durch die Kindklasse nicht erlaubt, und sie zu überschreiben schon gar nicht. Das Keyword final bedeutet ebenfalls, dass etwas nicht überschrieben werden darf.
Man kann final aber zu einer Signatur hinzufügen. Eine Methode kann also zum Beispiel public und final sein:
final public function example() { ... }

Eine Methode kann sogar final und private sein, auch wenn das keinen Vorteil bringt, da private den Effekt schon garantiert. final benutzt man immer dann, wenn man sicher stellen will, dass eine erbende Klasse etwas nicht verändert. Der Nutzen leuchtet wahrscheinlich nicht sofort ein. Er liegt darin, Programmierer davon abzuhalten, diese Methode zu überschreiben, weil man der Ansicht ist, dass eine Methode so und nur so funktionieren darf.
Die Auto-Klasse könnte also die Methode setBaujahr() als final deklarieren und wir könnten sie nicht in der MeinKind-Klasse überschreiben und verändern. Sinnvoll wäre das freilich nicht. Man kann auch gleich eine gesamte Klasse als final deklarieren und damit alle Methoden schützen:
final class Auto { ... }

Für dich als Anfänger besteht keine Notwendigkeit, final zu nutzen und ich rate auch erst einmal davon ab.

Namensräume


Fleißiger Padawan, willkommen zu deiner letzten Lektion! Wir haben im Verlauf dieses Tutorials mehrfach davon gesprochen, Klassen von Fremden zu nutzen. GitHub.com ist ein unermesslicher Quell solcher Fremdklassen und bietet viel nützliches. Es gibt dabei aber ein Problem, über das wir nicht hinwegsehen können: Wenn wir eine Klasse namens Auto auf GitHub finden und in unser PHP-Projekt einbinden, dann gilt das Gebot, diesen Code nicht zu ändern. Das sollte nach mehrmaliger Wiederholung inzwischen unmissverständlich klar sein.
Aber nicht alles, was wir auf GitHub finden, besteht nur aus einer einzigen Klasse! Oft bringen solche Komponenten dutzende von Klassen mit. Früher oder später werden wir an den Punkt kommen, an dem wir eine Klasse erstellen wollen, deren Name es schon gibt. Oder wir haben das Problem, dass wir ein Projekt mit der Auto-Klasse erstellt haben und erst spät merken, dass wir sie erweitern wollen. Dann müssen wir eine eigene Klasse mit einem anderen Namen wie MeinAuto wählen und "Auto" überall in unserem Code des restlichen Projekts durch "MeinAuto" ersetzen.
Bei beiden Problemen hilft die Nutzung von Namensräumen (englisch Namespaces). Das kann man sich wie Dateien und Ordner vorstellen: Wir legen unsere Dateien (die Klassen) in Ordnern (den Namensräumen ) ab. Diese Analogie geht so weit, dass es sogar ein bekanntes Konzept ist, Klassen-Dateien wirklich in Ordnern abzulegen, die wie ihre Namensräume heißen.
Angenommen, die Auto-Klasse wurde von einem Programmierer mit dem originellen Namen Max Mustermann erstellt. Die Auto-Klasse benutzt auch noch einige andere Hilfs-Klassen, zum Beispiel eine namens Kennzeichen, die Autokennzeichnen verwaltet. Das alles hat Max Mustermann in einem Repository (GitHub-Verzeichnis) mit dem Namen "Autoverwaltung" veröffentlicht. Dann könnte der Namensraum für die Auto-Klasse so lauten: "MaxMustermann\Autoverwaltung".
Damit die Klasse wirklich in diesem Namensraum liegt, schreiben wir ihn an den Anfang der Datei, in dem sie sich befindet (man legt für jede Klasse immer eine eigene PHP-Datei an):
<?php namespace MaxMustermann\Autoverwaltung;

class Auto {
...
}

Wenn wir jetzt ein neues Objekt der Auto-Klasse erzeugen wollen, funktioniert new Auto nicht mehr, wenn wir uns nicht innerhalb des Namensraums befinden, sondern wir müssen den Namensraum mit angeben:
$auto = new MaxMustermann\Autoverwaltung\Auto;

Es ist leicht vorstellbar, das Namenskonflikte bei Anwendung dieses Schemas unwahrscheinlich werden. Dieses Schema hat sich durchgesetzt, wobei "MaxMustermann" der GitHub-Name der Person ist. Da GitHub die Vergabe dieses Namens aber nur einmalig erlaubt, kann es weltweit nur einen MaxMustermann geben und Namenskonflikte sind tatsächlich ausgeschlossen. Wenn du allerdings ausgerechnet Max Mustermann heißt, musst du dir wohl oder übel ein Pseudonym zulegen oder den Namen modifizieren, wenn du dir einen GitHub-Account erstellen möchtest. Also jetzt mal im Ernst, es ist nie zu früh, sich seinen Namen bei GitHub zu reservieren.
Ebenso leicht vorstellbar ist, dass es wenig Freude bereitet, jedes mal den Namensraum anzugeben, wenn man auf eine Klasse referenzieren möchte. Deshalb kann man Namensräume innerhalb anderer Klassen mit use verfügbar machen - man nennt das auch importieren. Das ist nicht das selbe use wie beim Einbinden von Traits, eine gewisse Ähnlichkeit besteht aber. Man benutzt use wie folgt:
<?php
use MaxMustermann\Autoverwaltung;
class BeispielKlasse {
public function beispielMethode()
{
$autoObjekt = new Auto;
}
}

Das ist schon eine deutliche Vereinfachung. Indes, wir müssen immer noch für jede Klasse ein umständliches use-Statement anlegen. Oder sogar mehrere, wenn wir verschiedene Namesräume nutzen wollen:
<?php
use MaxMustermann\Autoverwaltung\Auto;
use SabineSonnenschein\Fahrzeugverwaltung\Auto;
use KarlaKompliziert\Nutzt\Namespaces\Mit\Sehr\Sehr\Vielen\Levels;

Selbstversändlich kann man auch gleich mehrere Namensräume importieren. Dann stehen wir aber wieder vor dem Problem, dass Klassen unterschiedlicher Namensräume den gleichen Namen tragen können. Daher kann mit dem Keyword as ein Alias vergeben werden, also ein Pseudonym:
<?php
use MaxMustermann\Autoverwaltung\Auto as MaxAuto;
use SabineSonnenschein\Autoverwaltung\Auto as SabineAuto;

Man kann nun "new MaxAuto" schreiben, um ein Objekt vom Typ "MaxMustermann\Autoverwaltung\Auto" zu erzeugen.
Ein letztes Problem ist aber noch zu lösen. Folgender Code funktioniert nicht:
<?php namespace MaxMustermann\Autoverwaltung;
class Auto {
public function beispielMethode()
{
throw new Exception('Das ist eine Test-Exception!');
}
}

Exception ist eine von PHP mitgelieferte Klasse, die im globalen Namensraum liegt, also keinen eigenen Namensraum hat. Wir befinden uns aber im Namensraum MaxMustermann\Autoverwaltung. Daher findet PHP die Klasse Exception nicht. Es gibt zwei Lösungswege: Entweder geben wir den Namensraum an, oder wir importieren die Klasse.
Die erste Variante funktioniert, indem wir "\" vor den Klassennamen setzen:
throw new \Exception('Das ist eine Test-Exception!');

Die zweite, indem wir wieder use nutzen:
<?php namespace MaxMustermann\Autoverwaltung;
use Exception;
class Auto {
public function beispielMethode()
{
throw new Exception('Das ist eine Test-Exception!');
}
}

Ende


Wir haben das Ende dieses Tutorials erreicht. Stell dir an dieser Stelle eine pompöse Zeremonie vor, in dem man dich zum OOP-Jedi erklärt! Zwei Themen habe ich ausgespart. Sie sind ebenfalls nicht unwichtig und ich würde raten, sie dir als nächstes in Eigenarbeit anzuschauen, sobald du wieder bereit bist, dem Ruf der OOP-Macht zu folgen: Abstrakte Klassen / Methoden und Interfaces.
Mit Hilfe des Keywords abstract kann man Klassen und Methoden (auch innerhalb von Traits) als abstrakt deklarieren. Diese enthalten dann keinen Code, der tatsächlich ausgeführt wird, sondern nur leere Signaturen von Methoden. Man macht das, wenn man erzwingen will, das erbende Klassen diese Methoden durch Überschreiben selber implementieren. Man erzeugt damit sozusagen einen Bauplan für Klassen: Es wird eine Hülle vorgegeben, der sie entsprechen müssen. Wenn man nicht nur einzelne Methoden, sondern eine ganze Klasse als abstrakt ausweist, kann man kein Objekt dieser Klasse erzeugen (da die Konstruktor-Methode ebenfalls abstrakt wird).
Interfaces sind ähnlich zu abstrakten Klassen. Ähnlich wie bei Traits kann man keine Objekte aus ihnen erzeugen. Klassen können anders als von abstrakten Klassen nicht von ihnen erben. Sie dienen nicht als Bauplan, eher als Vertrag: Wenn eine Klasse ein Interface implementiert, garantiert sie damit, dass sie dessen Methoden implementiert. Eine Klasse kann dazu mehrere Interfaces gleichzeitig implementieren. Die genauen Unterschiede beziehungsweise Vorteile herauszuarbeiten würde hier nun zu weit führen.

comments powered by Disqus

© 2014-2015 Christoph Konnertz.