Richtiger Umgang mit Exceptions?

Exceptions dienen dazu einen Fehlerfall zu signalisieren, der aber auf Wunsch vom Entwickler aufgefangen und dadurch die weitere Verarbeitung des Scripts fortgesetzt werden kann. So weit die Theorie, in der Praxis haben sich mir allerdings einige Fragestellungen ergeben.

Umsetzung von Exceptions in PHP

Exceptions werden in PHP mit der Basisklasse Exception und von dieser Klasse abgeleiteten Klassen erzeugt. Eine Exception, die nicht aufgefangen wird, endet in einem Fatal Error, was den Scriptabbruch zur Folge hat:

throw new Exception('Exception message');

Diese Klasse kann natürlich wie jede andere PHP-Klasse erweitert werden:

class CustomException extends Exception {
}
 
throw new CustomException ('Exception message');

Wenn man den aufgetretenen Fehler behandeln und anschließend die Scriptausführung fortsetzen möchte, kann man Exceptions auffangen:

try {
  // Code, der eine Exception werfen könnte
} catch(CustomException $e) {
  // Fehlerbehandlung
}

Was ist eine Ausnahme?

Da Exceptions schnell missbraucht werden können, indem sie zur Programmablaufsteuerung benutzt werden, bedarf es einer Definition, was überhaupt eine Ausnahme ist oder anders: Wann sollte man überhaupt eine Exception werfen?
Eine Ausnahme ist dann eine Ausnahme, wenn eines der folgenden Merkmale zutrifft:

  • Die Ausführbarkeit des folgenden Codes hat bestimmte Vorbedingungen (z.B. ein übergebener String darf nicht leer sein, der Datentyp muss den Erwartungen entsprechen).
  • Eine Funktion prüft vor der Rückgabe, ob das zurück zu gebende Ergebnis valide ist (richtiger Datentyp, Integrität der Daten gewährleistet).

Wenn beide Fälle nicht zutreffen, sollte keine Exception geworfen werden.
Klingt erstmal logisch, in der Praxis steht man allerdings hin und wieder vor schwierigen Entscheidungen. Ein Beispiel:

class UserFinder {
  public function findUserById($id) {
    $user = ... // User-Datensatz aus der Datenbank holen, der die ID $id hat
 
    if($user === null) {
      throw new UnderflowException('Could not find a user with id '.$id);
    }
  }
}

Theoretisch wäre es nicht nötig hier eine Exception zu werfen. Es könnte genauso gut null zurück gegeben werden.
In meinen Augen ist für diese Entscheidung wichtig, welcher Rückgabetyp per DocTag angegeben wurde. Ist es @return User, dann wäre eine Exception sinnvoll. Allerdings stellt null auch hier immer einen Sonderfall dar (wie auch bei TypeHints als Vorbedingung: Selbst wenn man per TypeHint eine Klasse definiert, kann immer auch null übergeben werden Wenn TypeHints bei der Funktionsdeklaration benutzt werden, kann null nicht übergeben werden, danke @Fabian_ikono). Was denkt ihr, ist in diesem Fall das Werfen einer Exception sinnvoll?

Wie viele Exception-Klassen sollte man erstellen?

Ein anderes Problem stellt die Spezifität einer Exception-Klasse dar. PHP bringt über die Standard PHP Library (SPL) bereits einige Exceptions mit. Hier gibt bereits für viele Fehlerfälle vorgefertigte Exception-Klassen, die man einfach nutzen kann.

Gehen wir einmal davon aus, dass wir uns dafür entschieden haben, ausschließlich die SPL-Exceptions zu nutzen. Wir haben eine Model-Klasse mit Setter-Methoden und in allen Setter-Methoden wird eine InvalidArgumentException geworfen, wenn der übergebene Wert nicht zu dem erwarteten passt, also eine Vorbedingung nicht erfüllt ist (oder anders gesagt: Würde man keine Exception werfen, entstünde ein inkonsistenter Zustand).

class Book {
  protected $title;
  protected $pages;
 
  public function setTitle($title) {
    if(empty($title)) {
      throw new InvalidArgumentException('Book title must not be empty');
    }
 
    $this->title = $title;
  }
 
  public function setPages($pages) {
    if(!is_int($pages)) {
      throw new InvalidArgumentException('Page count must be an integer number');
    }
 
    $this->pages = $pages;
  }
}

Ein Problem entsteht nun, wenn wir beide Setter in einem Try-Catch-Block aufrufen:

$book = new Book();
try {
  $book->setTitle($_GET['title']);
  $book->setPages($_GET['pages']);
} catch(InvalidArgumentException $e) {
  echo $e->getMessage();
}

Das Problem ist, dass wir nun zur Laufzeit nicht wissen, welcher der beiden Befehle fehlgeschlagen ist, weil beide die gleiche Exception werfen. Es gibt nun 3 Möglichkeiten, dieses Dilemma zu umgehen:

  • man macht 2 Try-Catch-Blöcke, sodass man immer weiß, welche Operation fehlgeschlagen ist
  • man erstellt 2 unterschiedliche Exception-Klassen, die beide im gleichen Try-Catch-Block abgefangen werden können
  • man nutzt den Fehler-Code, den man einer Exception zusätzlich mitgeben kann

Bei 2 Try-Catch-Blöcken hat man das Problem, dass man auch immer an eben diese Festlegung denken müsste. Noch schlimmer: Es kann passieren, dass in dem aufgerufenen Code weitere Funktionen aufgerufen werden und an irgendeiner Stelle eine InvalidArgumentException geworfen wird. Diesen Fehler wollte man eventuell gar nicht auffangen oder ihn zumindest loggen, da er eine tiefer liegende Ursache hat. Ein tiefer liegender Fehler würde somit ggf. kaschiert.

Die Variante verschiedene Exception-Klassen zu erstellen ist eleganter, allerdings ist hier die Frage, wie weit man das treiben möchte. Soll für jeden Fehlerfall eine eigene Exception-Klasse erstellt werden, sodass man stets genau weiß, an welcher Stelle der Fehler aufgetreten ist und man auf keinen Fall ungewollt einen anderen Fehler kaschiert? In diesem Fall würde die Anzahl an Exception-Klassen schnell sehr groß werden.
Ein Mittelweg könnte in diesem Fall das Schreiben von schichtenabhängigen Exceptions sein, auch bekannt unter Exception Chaining. Jede Schicht darf nur die Exceptions der direkt darunter liegenden Schicht auffangen. Wenn zum Beispiel eine Datei eingelesen werden soll, ist es der View-Klasse relativ egal, ob die Datei keine Leserechte (FilePermissionException) hat oder die Datei kaputt (FileDamagedException) ist. Für die View-Klasse ist nur wichtig, dass die Datei nicht lesbar ist. Deshalb müsste der Controller die genannten Exceptions auffangen und anschließend eine weitere Exception werfen. An diese neue Exception könnte man die ursprüngliche Exception anhängen (der 3. Konstruktorparameter der Exception-Klasse).

Und dann könnte man die Zuordnung noch über den Fehlercode lösen, aber damit habe ich noch nie gearbeitet. Habt ihr damit Erfahrungen?

Ich freue mich auf eure Meinung dazu in den Kommentaren!

Wohin packt man die Exception-Klassen?

Die zweite grundlegende Frage betrifft den Ort, an dem man die Exception-Klassen ablegt. Folgende Varianten sind mir in den Sinn gekommen:

  • man packt die Exception-Klassen in die gleiche Datei, in der sie auch geworfen werden.
  • Exception-Klassen kommen in eine separate Datei und liegen
    • im gleichen Ordner wie die aufrufende Klasse
    • im Ordner der Anwendungsschicht (Model-Exceptions, Routing-Exceptions usw.)
    • in einem globalen Exception-Ordner, der im obersten Quellcode-Ordner liegt
  • alle Exception-Klassen kommen in eine Datei

Meine präferierte Lösung war bisher immer die Exception-Klassen in separate Dateien und in einen Ordner namens “exceptions” zu legen, der unter dem Ordner für die jeweilige Schicht liegt (jede Schicht hat Ihre eigenen Exceptions).
Auch zu diesem Thema bin ich auf eure Kommentare gespannt!

Dieser Beitrag wurde in   PHP veröffentlicht.
Fügen Sie ein Lesezeichen für den   permanenten Link hinzu.

Jan hat 152 Beiträge geschrieben

2 Kommentare zu “Richtiger Umgang mit Exceptions?

  1. Adam Furmanczuk sagt:

    Zum Thema “Exception-Chaining”: Beim Fangen und erneuten Werfen von Exceptions sollte man extrem Vorsicht sein.
    Kann sein, wenn der Fehler durch die ganze Kette läuft, er nach dem Durchlaufen aller Schichten nicht mehr nachvollziehbar ist. (Thema “Exception in Exception”..)
    Meiner Meinung ist es besser in jeder Schicht Exceptions zu Cachten die auch dem “Kompetenz Grad” dieser Schicht entspricht. Ein Beispiel:
    Wir wollen ein Upload-Dienst, wo neben lokalem upload auch http referenzen gültig sind.
    Wir konzentrieren uns auf die 3 Schichten:
    – Oberfläche
    – Programm Ebene
    – Dateimanager
    In der Oberfläche wird gecacht ob die URL wohlgeformt ist.
    404 Fehler werden in der Obersten schicht geworfen aber nicht durch Programm Ebene gehandelt sondern in die Dateimanger ebene durchgereicht und dort erst gefangen.
    Der Dateimanager handlet die Exception und liefert an Programm Ebene eine Antwort der dann der Oberfläche kommuniziert.

    Wir haben in diesem Sinne dann keine Exception Kette. Fehler die für die jeweilige Schicht nicht interessant sind, gleiten eine Ebene tiefer.

    Muss meiner Meinung nach von Fall zu Fall betrachtet werden was sinnvoll ist.

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>