URL dieses Artikels:

zu Ausgabe: 3.2006
Auf Klassenfahrt
Ein Einstieg in die objektorientierte Programmierung mit PHP
von Arne Blankerts
Die Zeichen der Zeit sind klar: Objektorientierte Programmierung (OOP) gehört bereits zum Standard der Softwareentwicklung und da wollen Sie sicher nicht in der Vergangenheit stehen bleiben. Doch wie genau funktioniert OOP und was sind die Vorteile? In diesem Einführungsartikel erfahren Sie es!

Den sowohl klassischsten als auch technisch gruseligsten Fall einer Webseite stellt eine Mischung von mehr oder weniger validem HTML mit einigen Brocken eingebundenem PHP-Code dar. Der mögliche nächste Schritt der Evolution ist dann die Verwendung von ausgelagerten Funktionen, die jedoch für jedes Projekt angepasst oder neu geschrieben werden, obwohl sie eigentlich immer das gleiche machen. Besonders beliebt sind hier Formularverarbeitung und Datenbankoperationen. Spätestens seit Freigabe der 5.x-Reihe von PHP und der damit einhergehenden Überarbeitung des Objekt-Modells, sind an jeder Ecke OO-Frameworks und Komponenten zu bekommen, die alle das Entwickeln von Webseiten stark vereinfachen wollen und für deren sinnvolle Verwendung man natürlich zwingend in der OO-Entwicklung eingearbeitet sein sollte. Doch was genau ist ein Objekt eigentlich und wie wird es definiert? Nüchtern betrachtet ist ein Objekt nichts weiter als eine logische Zusammenfassung von Variablen und Funktionen unter einem gemeinsamen Dach - der so genannten Klasse. Sinn und Zweck dieser Klasse ist es im Idealfall, eine für die "Außenwelt" gekapselte Funktionalität bereitzustellen, ohne dabei ein tiefer gehendes Wissen über Interna zu erfordern. In PHP wird eine solche Klasse durch das Keyword 'class' eingeleitet, gefolgt vom Namen der Klasse sowie einem Paar geschweifter Klammern, die den Inhalt umfassen. Innerhalb der Klasse werden dann die Methoden - so heißen die Funktionen in der OOP - und Variablen fast wie gewohnt definiert.

Bezugspunkte
Da in der Praxis viele Methoden und Variablen nur für interne Zwecke benötigt werden und ein Aufruf bzw. eine Änderung von außen vielleicht sogar schädlich wäre, gibt es eine erweiterte Syntax, um "Zugriffsrechte" festzulegen. Die beiden gängigsten Varianten sind public für öffentliche, also von außen aufrufbare Methoden und Variablen, sowie analog dazu private für Elemente, die nur innerhalb der Klasse erreichbar sind. Mit diesem Wissen ausgerüstet, können wir jetzt schon unsere erste eigene Klasse definieren: Das Listing 1 zeigt eine einfache Klasse zur Ausführung einer mathematischen Operation. Der Faktor für die Multiplikation kann, da er als private markiert ist, nur über die public definierte Hilfsmethode setFactor() geändert werden. Damit die Klasse jedoch bei ihrer ersten Verwendung nicht ganz ohne Faktor dasteht, benutzen wir die "magische Methode" __construct(), um sicher zu stellen, dass wir auf jeden Fall einen sinnvollen Wert haben. Wie der Name der Methode bereits vermuten lässt, wird diese automatisch bei der Erzeugung der Klasse ausgeführt. Über diese so genannten Konstruktoren können beliebig viele interne Variablen gesetzt, Datenbankverbindungen geöffnet oder andere vorbereitende Maßnahmen getroffen werden, ohne die die Funktionalität der Klasse unter Umständen nicht gewährleistet ist. Im Code ist eine weitere Neuheit versteckt: Die implizite Variable $this dient als Behelfsreferenz, um innerhalb der Klasse quasi Selbstgespräche zu führen. Mittels $this->faktor lässt sich so auf die Klassenvariable $faktor zugreifen, und auch Methodenaufrufe wären auf diese Weise natürlich möglich. Passend zu den Konstruktoren gibt es seit PHP 5 übrigens auch das logische Gegenstück: den Destruktor. Er wird, sofern definiert, automatisch von PHP beim Beenden der Ausführung bzw. beim Verwerfen der Objektinstanz aufgerufen. Doch was ist eine Objektinstanz? Die reine Definition einer Klasse wie im Listing 1 ist alleine noch nicht lauffähig. Es gibt zwar durchaus Klassen, deren Methoden "statisch" aufgerufen werden können, in der Regel müssen Klassen jedoch instanziiert werden, um sie zu verwenden. Eine Instanz ist hierbei das eigentliche Objekt, welches sich aus der mittels class beschriebenen Definition ableitet. Der große Vorteil dieses Ansatzes ist, dass man von der gleichen Definition beliebig viele Instanzen erzeugen kann, ohne dass diese voneinander wissen oder sich beeinflussen. Das Listing 2 zeigt, wie genau dies in Code gegossen aussieht: Es werden zwei Instanzen erzeugt, die zwar die gleiche Definition verwenden, aber dennoch mit unterschiedlichen Faktoren beim Aufruf der Methode rechnen arbeiten - wie die Ausgaben verdeutlichen.

Listing 1: Klassendefinition

<?php

class multiplicator {

// Private Variable
private $faktor;

// magische Funktion - Konstruktor
public function __construct() {
$this->faktor=1;
}

// magische Funktion - Destruktor
public function setFaktor($f) {
$this->faktor=$f;
}

// Öffentliche Methode
public function rechnen($wert) {
return $this->faktor * $wert;
}

}

?>

Listing 2: Beispielanwendung

<?php

// klasse aus listing 1 laden
include('listing1.php');

// erste Instanz erzeugen
$objekt=new multiplicator();

// Mit Defaultfaktor rechnen
echo $objekt->rechnen(5)."\n";

// Faktor neu setzen und rechnen
$objekt->setFaktor(3);
echo $objekt->rechnen(5)."\n";


// zweite Instanz
$demo = new multiplicator();

// Auch hier wird der Defaultfaktor verwendet
echo $demo->rechnen(5);


// Versuch, den Faktor direkt zu setzen schlägt fehl
$demo->faktor=4;

?>

Familienbande
Mit dem bisher Gelernten haben wir die ersten Schritte in die objektorientierte Entwicklung getan - richtig spannend wird es jedoch erst jetzt: Der im Listing 1 definierten Klasse fehlt bisher jegliche Möglichkeit, uns mitzuteilen, auf was für einem Wert der interne Faktor zur Zeit eingestellt ist. Natürlich könnten wir dies einfach in der Urdefinition der Klasse hinzufügen - oder uns einer weiteren Funktionalität der OOP zuwenden: der Vererbung. Vererbung im Sinne der OOP heißt, eine neue Klassendefinition auf Basis einer anderen existierenden Klasse zu erstellen. Anstatt also die Urklasse zu erweitern, entwerfen wir eine abgeleitete Klasse mit der zusätzlichen Funktionalität. Um PHP wissen zu lassen, dass es sich um eine Erweiterung und nicht nur um eine einfache Neudefinition handelt, wird in der Klassendefinition ein extends eingefügt, gefolgt vom Namen der Klasse, welche wir erweitern. Um eine so erweiterte Klasse zu verwenden, muss natürlich auch die Instanziierung den Namen der neuen Klassendefinition verwenden. Doch wir haben erst einmal ein ganz anderes Problem: Da wir die Variable faktor auf private gestellt hatten, haben wir nicht nur von außen keinen Zugriff, auch alle abgeleiteten Klassen sind ausgesperrt. Um das zu umgehen, wird das Schlüsselwort private durch ein protected ausgetauscht. Variablen und Methoden, die mit diesem Schlüsselwort gekennzeichnet wurden, sind genau wie ihre privaten Verwandten nicht von außen, dafür aber von abgeleiteten Klassen aufrufbar. Es zeigt sich also schon in unserem kleinen Beispiel, dass die Vergabe von Zugriffsrechten wohl überlegt sein will. Zum Glück lässt sich dies in unserem Fall noch recht leicht anpassen. Das Listing 3 zeigt nun die um eine so genannte getter-Methode erweiterte, abgeleitete Version der Klasse aus Listing 1. Da es sich um eine Erweiterung handelt, sind alle Methoden und Variablen aus der vorherigen Klasse hier ebenfalls verfügbar, sodass wir den restlichen Code nicht weiter anpassen oder gar kopieren müssen. Ein sehr wichtiger Faktor und elementarer Vorteil der objektorientierten Entwicklung im Vergleich zur herkömmlichen prozeduralen Programmierung. Dass die Vererbung nicht nur auf das Hinzufügen von neuen Methoden begrenzt ist, zeigt die erneute Definition der Methode setFaktor: In der erweiterten Klasse wird die gleichnamige Funktion aus der Elternklasse überschrieben und durch eine eigene Funktionalität ersetzt. Um dennoch nicht das Rad neu zu erfinden, "importieren" wir die Eltern-Fassung der Funktion mittels eines statischen Aufrufs über das Konstrukt parent::setFaktor($var).

Listing 3: Vererbungsbeispiel

<?php

// klasse aus listing 1 laden
include('listing1.php');

// Abgeleitete Erweiterung
class multiplicator2 extends multiplicator {


// überschreiben und erweitern der setFaktor Methode
public function setFaktor($var) {
parent::setFaktor($var);
echo "Faktor erfolgreich eingestellt.\n";
}

// neue getter Methode
public function getFaktor() {
return $this->faktor;
}

}

// Instanz erzeugen
$objekt=new multiplicator2();

// Geerbte Funktion aufrufen
$objekt->setFaktor(5);

// Aufruf der neuen Getter-Methode
echo $objekt->getFaktor()."\n";

// Und Rechnen natürlich nicht vergessen...
echo ' -> '.$objekt->rechnen(4);

?>

Abstrakte Kunst
Bisher waren unsere Methoden und Variablen in Bezug auf die Vererbung eher neutral: Entweder es gab Zugriff - oder nicht. Wer wollte, konnte Funktionen überschreiben oder abgeleitete Varianten erzeugen. In der Praxis ist diese Flexibilität unter Umständen jedoch gar nicht gewünscht: Was, wenn eine Funktion in der Urklasse nur rudimentär vorhanden ist und daher in einer abgeleiteten Klasse überschrieben werden muss? Das Schlüsselwort abstract ist die Antwort. Sowohl eine einzelne Methode als auch die gesamte Klasse kann mit Voranstellen dieses Keywords eine direkte Instanziierung verhindern und eine Ableitung erzwingen. Sinn macht dieser Ansatz vor allem bei einer gemeinschaftlichen Basisklasse, deren Instanziierung an und für sich nicht sinnvoll wäre. Ein gängiges Einsatzgebiet für abstrakte Klassen sind "Grundgerüste" für Plug-ins: Die abstrakte Definition beinhaltet gerade soviel eigene Funktionalität, wie für die Verwendung als Plug-in notwendig sowie gegebenenfalls hilfreiche und häufig verwendete Methoden. Das logische Gegenstück zu abstract ist final: Eine final definierte Klasse oder Methode ist gegen Überschreiben bzw. Erweiterung geschützt - der Versuch, eine so definierte Methode zu überschreiben oder Klasse zu erweitern, wird mit einer Fehlermeldung verweigert. Ein denkbares Szenario für eine derartige Klasse wäre zum Beispiel ein Security-Layer, der zwar Funktionen für weitere Module bereitstellt, ansonsten aber unverändert eingesetzt werden soll und muss.

Anschluss gesucht
Zu den abstrakten und finalen Klassen gesellt sich eine weitere Alternative: Wenn Sie nur ein Grundgerüst an Funktionsdefinitionen vorgeben, jedoch keinen "echten" Code schreiben wollen, so kann anstelle einer abstrakten Klasse auch ein so genanntes "Interface" definiert werden. Ein Interface ist eine an sich leere Klasse, die nur aus Funktionsköpfen besteht, welche allerdings bei ihrer Verwendung exakt so umgesetzt werden müssen. Im Gegensatz zu einer "normalen" Klasse, die jeweils nur einfach weitervererbt werden kann, darf eine Klasse beliebig viele Interface-Definitionen zur selben Zeit implementieren. Dies macht die Verwendung für alle die Fälle interessant, in denen festgelegte Objekttypen gebraucht werden, welche eine vorgegebene API einhalten müssen, aber dennoch frei in ihrer Vererbungshierarchie sind. Das in Listing 4 abgedruckte Beispiel zeigt, wie alle diese bisher gelernten Elemente miteinander vereint aussehen könnten: Neben der Implementierung zweier einfachen Interfaces erweitert die nun finale Klasse die vorher abstrakte Basisklasse. Über den Operator instanceof stellen wir zudem sicher, dass beim Aufruf der Objektmethode Ausgabe eine Instanz des richtigen Typs vorliegt. Wie man an der If-Abfrage sehen kann, überprüft der Instanceof-Operator neben der aktuellen Klasse auch alle vorherigen Inkarnationen. Auf diese Weise lässt sich sicherstellen, dass auch abgeleitete Klassen "gefunden" werden. Eine Alternative zum instanceof-Operator kann in einigen Fallen auch das so genannte Type Hinting sein: Denn auch wenn PHP an sich eher wenig von festen Typen hält, so gibt es doch viele Situationen, in denen als Parameter einer Funktion nur ganz bestimmte Objekttypen sinnvoll sind - zum Beispiel eine getRow-Funktion einer Datenbankklasse, die ein Ergebnisobjekt erwartet. Anstatt vermeintlich umständlich den Typ per instanceof selbst zu verifizieren, kann man PHP im Funktionskopf auch eine Typenvorgabe vor dem eigentlichen Parameter mit auf den Weg geben. Falls der so implizit durchgeführte instanceof-Test von PHP fehlschlägt, gibt es eine entsprechende Fehlermeldung und die Ausführung des Programms wird abgebrochen.

Listing 4: OOP in Reinkultur?

<?php

// Interface Klasse 1
interface templateEngine {
public function Ausgabe($wert);
}

// Interface Klasse 2
interface berechnung {
public function addition($wert1,$wert2);
}

// Abstrakte Basisklasse
abstract class Basis {

protected $data;

public function vorlage($template) {
$this->data=$template;
}

}

// Komplexe Definition aus allen 3 Klassen in eine Neue
final class demo extends Basis implements templateEngine, berechnung {

// Methode für Interface 1
public function Ausgabe($wert) {
echo sprintf($this->data,$wert);
}

// Methode für Interface 2
public function addition($wert1,$wert2) {
return $wert1 + $wert2;
}
}


// Instanziieren
$objekt = new demo;
$objekt->vorlage('Das Ergebnis lautet: %s');

// Instanztyp testen
if ($objekt instanceof Basis && $objekt instanceof demo) {
$objekt->Ausgabe($objekt->addition(10,20));
}

?>

Das Leben ist einmalig
Nachdem wir die Grundlagen der objektorientierten Entwicklung jetzt erfolgreich überstanden haben, wenden wir uns zum guten Schluss noch einmal einem sehr häufig auftauchenden Problem zu: Wie stellt man am elegantesten sicher, dass es in der gesamten Anwendung nur genau eine Instanz von einer Klasse gibt? Die wohl häufigste und bekannteste Methode nennt sich kryptisch Singelton und macht sich die Tatsache zu Nutze, dass es instanzübergreifende statische Klassenvariablen und Methoden gibt. Ein Singleton-Pattern implementiert eine statische Methode zum Abruf der Instanz anstelle des sonst klassischen new-Konstrukts und speichert die Instanz-Variable in einer ebenfalls statischen Klassenvariablen ab. Was sich hier vielleicht kompliziert anhört, wird nach einem Blick auf die beispielhafte Implementierung in Listing 5 auf der Heft-CD leichter verständlich. Um zu garantieren, dass die Klasse auf herkömmliche Weise nicht instanziiert werden kann, setzen wir als "Krönung" die Methode __construct auf private. Dies stellt sicher, dass ein Aufruf von außen unmöglich ist - eine direkte Instanziierung wäre verhindert. Um nun eine Instanz von unserem Objekt zu bekommen, rufen wir die statische Methode getInstance auf, welche, sofern es sich um den ersten Aufruf handelt, eine neue Instanz erzeugt, intern abspeichert und eine Referenz auf das Objekt zurückliefert. Jeder weitere Aufruf gibt nur noch die bereits erzeugte Instanz zurück - unser Singelton ist fertig. Wie bereits beim Zugriff auf Funktionen früherer Inkarnationen mittels parent:: wird auch hier ein statischer Selektor verwendet, denn da wir uns nicht in einer Instanz bewegen, gibt es kein $this und wir müssen uns für den Zugriff auf statische Funktionen und Variablen mit self:: behelfen. Und noch ein Problem: Neben der im Kasten "Magische Funktionen" beschriebenen Funktion __autoload() gibt es auch eine nach OOP-Gesichtspunkten saubere Implementierung, um generisch und automatisch Quellcode nachzuladen: Die Fabrik-Methode, oder englisch das "Factory pattern": Dieses Pattern implementiert eine einzige statische Funktion, der man zum Beispiel nur den gewünschten Klassennamen übergibt und als Ergebnis eine passende Instanz erhält. Das Nachladen des benötigten Quelltextes übernimmt die Factory-Methode. Bestes und häufigstes Beispiel einer Factory-Implementierung dürften generische Datenbank-Wrapper sein, deren Factory-Methode man zum Beispiel einen DSN-String oder Datenbanktyp übergibt und als Ergebnis das passende Datenbank-Objekt beinhaltet: DB::factory('mysql');

Magische Funktionen
Neben den im Artikel erwähnten und im OOP-Modell zwingend vorgeschrieben Methoden für Konstruktion und Destruktion, gibt es in PHP eine ganze Reihe praktischer, so genannter magischer Funktionen [2], von denen hier eine Auswahl vorgestellt wird:

__call($method,$params): Ist die gewünschte Methode in einer Klasse nicht definiert, jedoch die magische Funktion __call vorhanden, so wird diese anstelle der eigentlichen Methode aufgerufen. Der ursprüngliche Name sowie die gegebenenfalls übergebenen Parameter werden mitgeliefert.

__get($var): Lesender Zugriff auf eine nicht bekannte Klassenvariable. Der Variablenname wird als Parameter übergeben. Mit dieser und der nachfolgenden Methode ist es möglich, "on-demand"-Variablen zu erzeugen und zu verwalten

__set($var,$wert): Schreibender Zugriff auf eine unbekannte Klassenvariable. Name und Wert werden als Parameter ebenfalls übergeben.

__isset($var): Wird ab PHP 5.1.0 bei Verwendung der PHP-Funktion isset auf eine eigentlich nicht gesetzte Klassenvariable aufgerufen.

__unset($var): Das Gegenstück zu __isset() und seit PHP 5.1.0 bei löschendem Zugriff via unset() auf eine nicht bekannte Klassenvariable aufgerufen.

__toString(): Diese Methode wird, sofern implementiert, beim Versuch aufgerufen, die Objektinstanz in einem String-Kontext zu verwenden - zum Beispiel echo $obj; Im Normalfall würde dies Objekt#id ausgeben, bei implementierter Methode __toString() kann dies durch eine sinnvollere Ausgabe ersetzt werden.

__sleep() und __wakeup(): PHP bietet die Möglichkeit, via serialize() und unserialize() alle Variablen und Objekte zum Zweck der Übermittlung oder Zwischenspeicherung in einen String zu übersetzen. Um einem Objekt die Möglichkeit eines kontrollierten "Shutdowns" zu geben, kann die Methode __sleep() implementiert werden, für ein kontrolliertes Aufwachen die analoge Funktion __wakeup().

__clone(): Wird ein Objekt über das Schlüsselwort clone im Speicher kopiert, wird nach erfolgter Dopplung diese Methode aufgerufen. So kann zum Beispiel eine neue eigene Datenbankverbindung aufgebaut oder andere administrative Tätigkeit ausgeführt werden.

__autoload($klasse): Diese magische Funktion wird außerhalb einer Klasse definiert und immer dann aufgerufen, wenn versucht wird, eine Klasse zu instanziieren, deren Definition noch nicht bekannt ist. Mithilfe dieser Funktion kann der notwendige Code "on-demand" nachgeladen werden; der Name der gesuchten Klasse wird als Parameter übergeben.

Schrittweise angekommen
Wohl jeder hat in der Vergangenheit schon einmal mit Arrays gearbeitet. Eine extrem praktische Vorgehensweise, um eine Schleife über alle Elemente eines Arrays zu implementieren, ist die Verwendung der Funktion foreach(). Dass man foreach auf Objekte anwenden kann, wissen jedoch die wenigsten. Wichtigster Unterschied: Wird beim Einsatz von Arrays auf einer Kopie gearbeitet, liefert ein foreach über ein Objekt eine Referenz auf alle sichtbaren - sprich public gesetzten - Variablen der Klasse. Dies ist natürlich vor allem für verschachtelte Objekte sinnvoll, um so zum Beispiel eine Funktion auf alle Elemente in der Struktur anzuwenden. Dieses Verhalten wird "Iteration" genannt und insbesondere von der SPL [5], der Standard Procedures Library, verwendet, welche im letzten PHP Magazin ausführlich vorgestellt wurde. Nach diesem Crashkurs in Sachen Objektorientierung sind Sie - wenn nicht gar in der Zukunft - so doch zumindest in der Gegenwart angekommen. Neben den vielen überarbeiteten und mit OO-API ausgestatteten Erweiterungen von PHP stehen Ihnen spätestens jetzt auch die vielen neuen Frameworks und OO-Komponenten offen. Da objektorientierte Entwicklung quasi automatisch zur Trennung von Code und Design erzieht, entsteht im selben Atemzug automatisch sauberer und wiederverwendbar Quelltext, der in vielen Projekten eingesetzt werden kann. Wenn das kein Grund ist, sich mit voller Kraft der objektorientierten Programmierung zu widmen!

OOP in PHP Version 4?
Mit der Version 5 von PHP wurde das Objektmodell grundlegend überarbeitet. Doch auch wenn sich dieser Kurs aufgrund der zukunftsweisenden neuen Struktur auf diese Version konzentriert, so ist natürlich auch mit PHP 4 ein objektorientiertes Arbeiten möglich. Die Version 4 kennt jedoch noch keine Unterscheidung von public, private oder protected: Variablen werden schlicht durch das Schlüsselwort var definiert, was in PHP 5 einer public-Variablen gleich kommt. Auch private, abstrakte oder finale Methoden kennt PHP 4 nicht - eine einmal definierte Methode ist automatisch immer öffentlich. Es hat sich daher ein Quasi-Standard etabliert, bei dem private Methoden oder Variablen visuell durch ein vorgestelltes "_" markiert werden, um ihren Status als "private" zu verdeutlichen - ein wirklicher Schutz war und ist dabei jedoch nicht möglich. Wenn Sie bisher noch nie mit PHP in objektorientierter Weise gearbeitet haben, sollten Sie unbedingt und sofern irgend möglich auf PHP 5 setzen, denn das überarbeitete Objektmodell macht den Einsatz von OOP erst richtig sinnvoll möglich - ganz davon abgesehen, dass die Verarbeitungsgeschwindigkeit deutlich gesteigert wurde und auch viele der grundlegenden PHP-Erweiterungen jetzt eine OOP-API aufweisen. Zu den bekanntesten Beispielen zählen die ebenfalls überarbeitete DOM-Erweiterung, welche jetzt w3c-konform aufgebaut ist, sowie die auf MySQL 4.1 und später zugeschnittene neue MySQL-Erweiterung mysqli (für mysql-improved). Auch die leistungsfähigen Iteratoren der SPL sind erst mit PHP 5 verfügbar und ersparen einiges an ansonsten umständlicher Handarbeit. Neben der Unterscheidung der unterstützten Schlüsselworte wurde auch das Vorgehen geändert, wie Daten übergeben werden: In PHP 4 werden - sofern nicht explizit als Referenz erzwungen - Objekte in Kopie an eine Funktion übermittelt, in PHP 5 ist jedoch die Übergabe als Referenz default. Um eine Kopie zu erhalten, muss dieses explizit per clone erzwungen werden.
Arne Blankerts (theseer@php.net) ist Leiter der Entwicklung bei der SalesEmotion AdSolutions GmbH in Hamburg. Deren unter Open-Source-Lizenz vertriebenes SiteSystem fCMS v5 [3] basiert vollständig auf PHP5, dessen neuem Objektmodell, und setzt voll auf Modularität unter Verwendung der hier gezeigten Techniken.

Links

© 2004 Software & Support Verlag GmbH. Vervielfältigung nur mit Genehmigung des Verlags. Fragen?