Mit PHP 5 wurde dem Entwickler ein Werkzeug zur Verfügung gestellt, dass es in anderen objektorientierten Sprachen wie C++ und Python schon lange gibt: Exceptions. Mit diesen hat der Entwickler die Möglichkeit, auf ungewollte Ereignisse zu reagieren. Exceptions werden “geworfen”, das bedeutet, sie unterbrechen den normalen Programmfluss und behandeln eine eventuelle Ausnahme, die den weiteren Programmverlauf stören können. In einer Desktopapplikation beispielsweise könnten Exceptions genutzt werden, um noch vor dem Windows-Fehlermeldungsfenster ein eigenes zu generieren, in der der Benutzer die Möglichkeit hat, einen Fehlerbericht an die Softwarefirma zu schicken. Ohne diese Ausnahmebehandlung würde das Programm mit einer Windows-Standardmeldung sang- und klanglos abstürzen und der User wäre verwirrt. Auch in PHP lassen sich Exceptions sinnvoll nutzen. Mit Exceptions sind sicherlich schon einmal alle Entwickler in Berührung gekommen, die ein MySQL-Statement via PDO mit einer fehlerhaften Syntax abgegeben haben.
Im unten aufgeführten Beispiel mal ein ganz einfacher Anwendungsfall – zunächst ohne Fehlerbehandlung: Aus einer Zahl soll eine Wurzel gezogen werden:
$a = sqrt(9); // Wert von $a: 3 $b = sqrt(-9); // Wert von $a: NaN
Warum Exceptions?
Falls wir im weiteren Programmverlauf mit der Variable $b weiterrechnen wollen, kommt es mit Sicherheit zu einer Störung, da $b nun keinen gültigen numerischen Wert mehr hat. Um den Programmverlauf zu unterbrechen, könnte der Entwickler nun eine Exception werfen:
function wurzel($nr) {
if(is_numeric($nr) && $nr >= 0) {
return sqrt($nr);
} else {
throw new Exception('Zahl für Wurzel-Funktion ist ungültig!');
}
}
$a = wurzel(9);
$b = wurzel(-9);
In diesem Fall wird das Programm sofort mit einer Meldung unterbrochen, und der Entwickler sieht sofort, wo es zu Problemen kam. An dieser Stelle kommt oft der berechtigte Einwurf, dass man an dieser Stelle ja auch einfach exit() zum Programmabruch nehmen kann.
Diese Annahme kann ich verstehen, da unser oben genanntes Beispiel noch recht sinnfrei ist. Richtig interessant werden Exceptions aber erst durch zwei Tatsachen:
- Exceptions können abgefangen und speziell behandelt werden
- Exceptions sind objektorientiert und können nahezu beliebig für den eigenen Zweck erweitert werden
Exceptions abfangen
Doch wie fangen wir solche Exceptions ab? Dies geschieht mit einem try()-catch()-Block. Zur Veranschaulichung erweiterte ich unsere Funktion wurzel() um den try-catch-Block:
function wurzel($nr) {
if(is_numeric($nr) && $nr >= 0) {
return sqrt($nr);
} else {
throw new Exception('Zahl ist ungültig!');
}
}
try {
$w1 = wurzel(-9);
$w2 = wurzel(9);
} catch (Exception $e) {
echo 'Da ist was schiefgelaufen: ' . $e->getMessage();
}
// Das Programm wird trotz Exception weiter ausgeführt
echo $w2;
Ich habe hier bewusst zuerst die Wurzel-Funktion (absichtlich) fehlerhaft benutzt (mit dem Wert -9). Weil wir die Exception abgefangen haben, können wir das Programm gewohnt weiterlaufen lassen. Gleichzeitig liefern wir dem Benutzer die Meldung über den Konstruktor der Exception-Klasse.
Der try/catch-Block bestimmt hier maßgeblich den weitern Programmverlauf: Im try-Block werden Aktionen durchgeführt, in denen der Entwickler eine Exception vermutet und abfangen möchte. Ohne das Abfangen der Exception würde diese zwar ausgegeben werden, aber das Programm ist abgebrochen.
Danke an Mykon für den Hinweis, dass der try/catch-Block natürlich nicht von der Funktion selber definiert werden sollte.
Dosierung
Das Benutzen von try/catch-Anweisungen erfordert also eine gewisse Disziplin vom Entwickler, kann aber auch schnell zu unübersichtlichem Programmcode führen. Wann Exceptions abefangen werden sollten, wird immer wieder diskutiert. Es gibt also keine Faustregel, wann Exceptions gesetzt werden sollen und wann andere Mechanismen wie “exit()” oder “throw_error()” zur Geltung kommen könnten.
Wann sollten Exceptions genutzt werden?
Zunächst einmal müssen wir genau definieren, was eine Exception ist – übersetzt handelt es sich um eine Ausnahme. Allgemein wird davon gesprochen, dass Ausnahmen immer nur im Fehlerfalle geschehen. Ich habe aber auch schon Blogkommentare und Forenbeiträge gelesen, in denen die Meinung vertreten wird, dass es sich bei Exceptions auch um Ausnahmen handelt, die nicht unbedingt einen Fehler bedeuten. Beispiel: Der Millionste User eine Webseite. Dieser ist eine Ausnahme und sollte dann, laut Meinung der Ersteller, eine Exception verursachen. Soweit möchte ich nicht gehen, denn auch wenn es sich bei dem Millionsten User sicherlich um eine Ausnahme handelt, sollte dieser Fall als Feature in der Programmlogik abgewickelt werden und nicht von einer Exception-Instanz.
So, wann soll eine Exception geworfen werden? Die Antwort “Im Falle eines Fehlers” ist viel zu allgemein, denn ein nicht unwesentlicher Bestandteil bei der Entwicklung von Webapplikation beschäftigt sich bereits mit der Fehlerbehandung – hier mal ein grobes Beispiel: Ein User wird anhand seiner ID aus der Datenbank angefordert. Hier wird die Abfrage abgewickelt:
public function checkLogin($mail, $pass) {
$q = 'SELECT id, nickname, email FROM user WHERE email = :email AND pass = :pass LIMIT 1;';
$sth = $this->conn->prepare($q);
$sth->execute(array(
':email' => $mail,
':pass' => $pass,
));
return $sth->fetchObject();
}
Und hier soll die Abfrage verarbeitet werden:
public function login($username, $password) {
$user = $this->user->checkLogin($username, $password);
if empty($user) {
echo 'Die Kombination aus Benutzername und Passwort ist uns nicht bekannt.';
} else {
echo 'Hallo ' . $user->nickname . '!';
}
}
Hier wird keine Exception geworfen, da es sich nicht um eine Ausnahme handelt, sondern um einen Fehler der mit einer gewissen Wahrscheinlichkeit eintreffen kann. Die Frage ist also: Kann ich hier mit einem Fehler rechnen? Wie wahrscheinlich ist es, dass der Fehler auftreten kann? Rechne ich überhaupt mit einem möglichen Problem an dieser Stelle? Je eher alle diese Fragen mit “Ja” beantwortet werden können, desto eher ist hier von Exceptions abzuraten. Ein fehlerhafter Login, das Nichtvorhandensein einer angefragten Datei, der Fehler beim DB-Insert – all das sind meines Erachtens nach Probleme, die keiner Exception würdig sind.
Hier ist also etwas Fingerspitzengefühl gefragt. Ich sage euch also, wofür ich Exceptions benutze. Das muss nicht bedeuten, dass es an dieser Stelle richtig ist, denn ich bin natürlich nicht perfekt.
Für einen Kunden sollte ich einmal die Generierung einer PDF-Datei anhand diverser Parameter realisieren. Wurde ein Parameter nicht gesetzt, so wird ein vorher definierter Default-Wert benutzt. Wenn es bei der Generierung der PDF-Datei nun trotzdem zu einem Fehler gekommen wäre (Speicherplatz ausgeschöpft?), so würde dies eine Exception abfangen und ich werde automatisch benachrichtigt. Weiter sorgt die Exception dafür, dass der Benutzer beim Generieren der PDF eine Fehlermeldung erhält, dass es zu einem Problem gekommen ist. Die Übersichtsseite wird wieder aufgerufen.
Einen weiteren Fall für die Benutzung von Exceptions kann ich mit meinem eigenen Framework aufzeigen: Anhand der URL wird der zu ladene Controller bestimmt. Ist der Controller nicht vorhanden, so wird der Error-Controller aufgerufen, der zuvor in der Config-Datei angegeben wurde. Wenn allerdings auch dieser Controller nicht gefunden werden kann, so wird eine Exception produziert.
Fazit
Wie man es dreht und wendet, sicherlich ist der Einsatz jeder Exception diskussionswürdig. Im Optimalfall hat eine Anwendung natürlich keinen einzigen Fehler, aber davon wird sich kein Entwickler dieser Welt freisprechen können. Es kommt also darauf an, jeden auftretenden Stolperstein abzufragen, um darauf adäquat zu reagieren. Erst jenseits dieser Fehlerquellen sollten Exceptions genutzt werden – dann sind sie nämlich eine Ausnahme. Exceptions dienen der Fehlerbehandlung. Mit exit() oder throw_error() kann man lediglich auf diese reagieren, ohne entsprechende Maßnahmen zu diesem Problem einzuleiten.



Dein Try-Catch ist völlig fehl am Platz in der Funktion “wurzel”. Die Funktion an sich, sollte bloss die Exception werfen. Der Programmierer der die Funktion “wurzel” verwendet, entscheidet ob er sie fangen will, und wenn ja, was damit geschehen soll.
Da hast du Recht – ich werde dies baldmöglichst ändern.
Das Programm im Abschnitt “Exceptions abfangen” bricht mit der Meldung “PHP Notice: Undefined variable: w2″ ab.
Was da nämlich passiert ist sogenanntes Stack Unwinding: sobald eine Ausnahme geworfen wird, werden vom Call Stack so lange Stack Frames entfernt, bis eine try-catch-Block erreicht wurde, der die Ausnahme fängt. Nach Abarbeitung des catch-Teils wird dann mit der nächsten Anweisung nach dem catch-Teil weitergerabeitet. In obigem Programm wird also die Anweisung “$w2 = wurzel(9)” überhaupt nicht ausgeführt, so daß $w2 nie definiert ist.
Dieser Sachverhalt kommt in deinem Beitrag nicht deutlich genug heraus.
Eine Faustregel zu Beurteilung ob eine Exception geworfen werden sollte ist: “Eine Funktion sollte eine Exception werfen, wenn sie die Nachbedingung nicht erfüllen kann”.
Durch diese Faustregel werden die Begriffe “Fehler” und “Ausnahme” eliminiert, die mehrere Bedeutungen haben können. Die Kunst besteht nun darin, die Nachbedingungen so festzulegen, dass die Funktion für den Benutzer intuitiv ist.
Eine Funktion, die Anmeldedaten auf Gültigkeit überprüft, könnte die Nachbedingung haben, dass bei einem Rückgabewert von “true” ein Benutzer in der Datenbank existiert, der die übergebenen Anmeldedaten hat und ansonsten “false” zurückgeliefert wird. Dann müsste eine Ausnahme geworfen werden, wenn zum Beispiel die Prüfung aufgrund fehlender Datenbankverbindung nicht durchgeführt werden kann.
Eng verzahnt mit Nachbedingungen und Exceptions sind Vorbedinungen: eine Funktion ist nur dann verpflichtet, Nachbedingungen einzuhalten, wenn ihre Vorbedingungen erfüllt sind. Es gibt zwei Vorgehen bei Verletzung der Vorbedingungen:
- In der defensiven Programmierung wird eine Exception geworfen. Dazu muss die Funktion selbst prüfen ob ihre Vorbedingungen erfüllt sind.
- Im Design by Contract bleibt es unspezifiziert, wie sich die Funktion verhält. Dann ist der Aufrufer verpflichtet, die Einhaltung der Vorbedingungen sicherzustellen und die Funktion kann unter der Annahme arbeiten, die Vorbedinungen seien erfüllt.