Sven Rautenberg: PDOException die() unformatiert!

Beitrag lesen

Moin!

try {
  $this->DBH = new PDO( $dsn, $user, $pw );
  $this->DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
} catch ( PDOException $err ) {
  $this->getError();
}

try {
  ...
} catch ( PDOException $err ) {
  die( $string );
}

Wenn Code eine Exception wirft, will er damit der aufrufenden Stelle mitteilen: "Sorry, die Funktionalität, die du gerade aktiviert hast, funktioniert nicht - ich habe ohne Ergebnis abgebrochen, und du solltest das auch tun."

Wenn die Stelle, die die Exception wirft, in keinerlei try/catch-Block steht, wird sie durch alle Aufrufe im Stack "nach oben bubbeln", dabei auch die oberste Ebene des aufgerufenen PHP-Skripts erreichen und beim PHP-Interpreter landen, der diese nicht gefangene Exception mit einem "fatal error" quittiert. Das ist eigentlich ein wünschenswerter Zustand, denn offenbar wurde für diesen Fehlerfall keinerlei Alternativstrategie programmiert.

Diese Alternativstrategien sind pauschal in zwei Gruppen zu unterteilen:

  1. Alternativen, die sich mit den Details auskennen und ein Notprogramm aus dem Hut zaubern können.
  2. Alternativen, die nur den Mangel verwalten und deshalb nur eine Fehlermeldung liefern können.

Fangen wir mit Nr. 2 zuerst an: Um grundsätzlich zu verhindern, dass ungefangene PHP-Exceptions einen "fatal error" erzeugen, gibt es als letzte Bastion die Möglichkeit, eine Funktion zu registrieren, die sich um alle nicht gefangenen Exceptions kümmert. Siehe set_exception_handler().

Die Funktion, deren Aufruf du registrieren musst, muss nur einen einzigen Parameter, nämlich eine Klasse vom Typ Exception (bzw. ab PHP 7 besser das Interface Throwable), empfangen, und darf danach im Prinzip alles tun, was man in so einer Situation tun kann - mit einer Einschränkung: Wenn komplizierte Dinge getan würden, die ihrerseits wieder eine Exception triggern, die nicht gefangen wird, landet man kein zweites Mal im Exception-Handler. Also ist es sinnvoll, nichts allzu komplexes zu tun. Man kann auf dem Produktivserver eine schlichte HTML-Nachricht der Art "Irgendetwas unerwartetes ist fehlgeschlagen - unsere Admins sind bereits benachrichtigt." an den User zurückgeben, und auf dem Entwicklungsserver auch noch direkt den gesamten Stacktrace inklusive Exception-Message ausgeben, was das schnelle Reagieren beim Entwickeln erleichtert. (Die Produktionsmeldung erfordert natürlich, dass sich jemand die Serverlogs anschaut - mit mail() eine Nachricht zu versenden dürfte manchmal funktionieren, aber was, wenn die Exception genau deswegen kam, weil keine Mail versendbar war?)

Zu derselben Gruppe gehört sowas hier:

# index.php
require('../vendor/autoload.php'); // Composer
$config = require('../config/di.php'); // Dependency Injection

try {
  $app = new App($config);
  $app->run();
} catch (\Exception $e) {
  echo "<html><body>Irgendwas ist schiefgelaufen...</body></html>";
  exit(1); // Abbrechen
}

// irgendwelches post-processing oder Aufräumen bei erfolgreichem Request

Die andere Gruppe von Exception-Handling versucht, als Alternative ein Notprogramm aus dem Hut zu zaubern. Das funktioniert aber natürlich nur, wenn es etwas sinnvolles zu tun gibt. Beispielsweise könnte auf einer Webseite das Wetter oder irgendein Aktienkurs eingeblendet werden und soll beispielsweise jede Minute neu vom Quellserver bezogen werden, und den Rest der Minute aus dem Cache kommen.

Wenn das Neuholen der Daten scheitert, könnte man grundsätzlich ja die schon existierenden Daten weiterverwenden (Datum der letzten Aktualisierung wird einfach mit ausgegeben) - und wenn keine Daten vorliegen, könnte man irgendeine Meldung anzeigen oder die Info ganz weglassen, ohne dass es die komplette Seite am Anzeigen hindert.

Meist ist das, was man tut, allerdings wichtig, und das Notfallprogramm besteht einfach darin, der darüberliegenden Softwareschicht mitzuteilen, dass es nicht funktioniert hat. Die soll dann entscheiden, ob es SO wichtig war, dass die Aufgabe komplett abgebrochen werden muss, oder ob es eine Alternative gibt.

Gutes Exception-Handling zeichnet sich dadurch aus, dass man eben gerade kein "Pokemon-Exception-Handling: Catch them all" macht (wie im zweiten Codebeispiel), denn wenn EIN catch für alle Exceptions zuständig ist, kann man entweder nur genau ein Verhalten für alle Exceptions programmieren (hier: Fehlermeldung zeigen und abbrechen), oder es wird sehr sehr kompliziert, für jede individuelle Exception eine Sonderbehandlung zu schreiben. Genauer gesagt ist die oberste Applikationsschicht der falsche Ort für die Details eines Notfallplans, denn die Alternativdaten werden irgendwo im Inneren der Applikation benötigt - und dort wieder hinzuspringen dürfte unmöglich sein.

Deshalb bringt jede Softwareschicht am Besten ihre eigenen Exceptions mit. Wie du bei PDO gesehen hast, existiert die PDOException: Nur PDO wirft diese Exceptions, und Code, der allgemein Exceptions fangen kann, könnte erkennen, dass beim Auftreten einer PDOException offensichtlich etwas mit der Datenbank nicht stimmt.

Die Template-Engine "Twig" ebenfalls eine eigene Exception definiert, und im Verzeichnis "Error" noch drei Untervarianten davon. Ein Programmteil, der jetzt irgendwas mit Twig-Templates macht, könnte diesen Twig_Error fangen und anders behandeln, als eine PDOException. Andere Softwarepakete haben üblicherweise ebenfalls auch eigene Exceptions definiert.

Was üblicherweise passiert: Code ruft eine Funktion einer anderen Schicht auf und fängt eine Exception. Der Code kann deshalb nicht normal weiterarbeiten und muss das weiter nach oben melden. Zwei Möglichkeiten bestehen:

  1. Man kann auf das Fangen der Exception verzichten, oder mit catch (Exception $e) { throw $e; } die gefangene Exception wieder werfen (letzteres ist sinnlos, wenn man nichts zusätzlich macht, es verwirrt nur den codelesenden Entwickler etwas). Dann sieht die Softwareschicht darüber, was diese Schicht für Implementierungsdetails darunter aufgerufen hat - aus Abstraktionsgründen ist das eher schlecht.

  2. Diese Softwareschicht kann auch eine eigene Exception werfen und damit die Implementierungsdetails (z.B. die Verwendung von PDO) vor der aufrufenden Schicht verstecken. Seit PHP 5.3 kann man beim Neuerzeugen von Exceptions die vorhergehende Exception als Parameter dem Konstruktor mitgeben, es muss also keine Information verloren gehen. Der Vorteil ist, dass die neue Exception eine detailiertere Beschreibung des Fehlerzustands enthalten kann, weil höhere Softwareschichten in der Regel abstraktere Funktionen enthalten. Als Entwickler muss man sich also nicht mit "Ein SQL-Query auf die DB ging schief" herumschlagen (Welcher denn? Es gibt vermutlich hundert Querys, und manche sehen fast gleich aus bzw. SIND gleich, werden nur von unterschiedlichen Stellen aus aufgerufen), sondern kriegt "Beim Lesen der User-Tabelle fürs Login hat was nicht funktioniert."

Grüße Sven