StudipCsFehlerbehandlung

Fehlerbehandlung

Coding Style Index?

1.  Fehlerbehandlung mit Ausnahmen

Häufig kommt es vor, dass ein Fehler in dem Kontext, in dem er auftritt (beispielsweise in einer Methode), nicht behandelt werden kann. Ein Grund hierfür kann sein, dass die für eine korrekte Behandlung benötigten Informationen nicht verfügbar sind. Vielmehr muss der Fehler an die aufrufende Instanz des Kontextes, in dem er aufgetreten ist, zur Behandlung signalisiert werden. Diese Signalisierung eines Fehlers an den aufrufenden Kontext kann auf zwei Arten erfolgen: "in-band" oder "out-of-band".

Bei der "In-band"-Methode wird ein Fehler durch einen ausgezeichneten Wert des Wertebereiches des Rückgabewertes signalisiert. So liefert die getpriority()-Funktion der UNIX-C-Bibliothek beispielsweise einen Wert vom Typ int als Ergebnis. Wie in der UNIX-C-Bibliothek üblich, dient auch hier der Wert -1 als Signal für einen Fehler. Dies führt jedoch zu einem Problem, da -1 ein legitimer Rückgabewert für getpriority() ist. Um unterscheiden zu können, ob eine zurückgegebene -1 einen Fehler signalisiert oder ein normaler Rückgabewert ist, muss der Aufrufer von getpriority() die Variable errno zu Rate ziehen.

Auszug aus der Manual-Page zu getpriority()

Since getpriority can legitimately return the value -1, it is necessary
to clear the external variable errno prior to the call, then  check  it
afterwards to determine if a -1 is an error or a legitimate value.  The
setpriority call returns 0 if there is no error, or -1 if there is.

Durch die Überprüfung einer zusätzlichen Variablen ist die "In-band"-Methode nicht nur umständlich, sondern auch häufig Grund für Programmierfehler, da keine syntaktische Unterscheidung von Fehlerzustand und Rückgabewert möglich ist. Ausnahmen, im Englischen Exceptions genannt, sind eine Möglichkeit, Fehlerzustände "out-of-band" zu kommunizieren. Hierbei treten die genannten "In-band"-Probleme nicht auf.

Code, der für die Behandlung einer Ausnahme typisch ist, wird in so genannten Exception-Klassen gekapselt, die sich in PHP von der Standardklasse Exception ableiten müssen.

Abbildung zeigt das UML-Klassendiagramm der Standardklasse Exception:

Attach:ClassDiagramException.png Δ

getCode() liefert den optionalen numerischen Code der Ausnahme. Dieser Code kann beispielsweise einem Fehlercode entsprechen, wie ihn die PHP-Datenbankschnittstellen im Fehlerfall liefern.

getFile() liefert den Namen der Datei, in deren Quelltext die durch das Exception-Objekt repräsentierte Ausnahme ausgelöst wurde.

getLine() liefert die Nummer der Zeile, in der die durch das Exception-Objekt repräsentierte Ausnahme ausgelöst wurde.

getMessage() liefert die optionale Nachricht der Ausnahme. Diese Nachricht kann beispielsweise über den Konstruktor einer Exception-Klasse gesetzt werden.

getTrace() liefert die Aufrufliste (englisch: Stack-Trace) bis zur Auslösung der Ausnahme als Array. Dieses hat dieselbe Struktur wie das Ergebnis der PHP-Funktion debug_backtrace().

getTraceAsString() liefert die Aufrufliste als String.

Die Methoden der Klasse Exception sind final und können daher in einer Kindklasse nicht redefiniert werden.

Eine Ausnahme (ein Objekt einer von Exception abgeleiteten Klasse) wird mit dem throw-Operator "geworfen". Die Ausführung des aktuellen Programmkontextes wird hierdurch abgebrochen und die Ausnahme wird an den aufrufenden Kontext zurückgegeben.

Wie in C# so wird auch in PHP (im Gegensatz zu beispielsweise Java) in der Signatur einer Methode nicht deklariert, welche Ausnahmen in ihrem Rumpf ausgelöst werden können. Ausnahmen sind in PHP "unchecked", was die Erweiterung und Vererbung von Klassen erleichtert.

Das folgende Beispiel zeigt die Klasse DB_Exception, die wir im Folgenden in unserer Datenbankklasse einsetzen möchten. Diese Klasse definiert keine eigene Funktionalität. Sie wird nur eingeführt, damit bei der Ausnahmenbehandlung anhand des Exception-Typs zwischen Datenbankfehlern (repräsentiert durch DB_Exception) und allgemeinen Fehlern (Exception) unterschieden werden kann.

Beispiel: Die Klasse DB_Exception

  1. <?php
  2. class DB_Exception extends Exception {}
  3. ?>

Das Beispiel zeigt, wie in den Methoden connect() und query() mit dem throw-Operator bei Auftreten eines Fehlers eine Ausnahme, in diesem Fall vom Typ DB_Exception, ausgelöst wird.

Beispiel: Fehlerbehandlung mit Ausnahmen

  1. <?php
  2. class DB_MySQL {
  3.   // ...
  4.  
  5.   public function connect($host, $database, $user, $pass) {
  6.     $this->connection = @mysql_connect(
  7.       $host,
  8.       $user,
  9.       $pass,
  10.       TRUE
  11.     );
  12.  
  13.     if (!$this->connection) {
  14.       throw new DB_Exception(
  15.         'Konnte keine Verbindung zur Datenbank aufbauen.',
  16.         @mysql_errno()
  17.       );
  18.     }
  19.  
  20.     if (!@mysql_select_db($database, $this->connection)) {
  21.       throw new DB_Exception(
  22.         'Konnte die gewünschte Datenbank nicht auswählen.',
  23.         @mysql_errno($this->connection)
  24.       );
  25.     }
  26.   }
  27.  
  28.   // ...
  29.  
  30.   public function query($query) {
  31.     if (is_resource($this->connection)) {
  32.       if (is_resource($this->result)) {
  33.         @mysql_free_result($this->result);
  34.       }
  35.  
  36.       $this->result = @mysql_query(
  37.         $query,
  38.         $this->connection
  39.       );
  40.  
  41.       if (!$this->result) {
  42.         throw new DB_Exception(
  43.           @mysql_error($this->connection),
  44.           @mysql_errno($this->connection)
  45.         );
  46.       }
  47.     }
  48.   }
  49.  
  50.   // ...
  51. }
  52. ?>

Ein Codeblock, in dem Ausnahmen ausgelöst werden können, wird mit dem try-Schlüsselwort ausgezeichnet und mit geschweiften Klammern umschlossen. Direkt nach diesem Block steht eine beliebige Anzahl von catch-Blöcken, für jede mögliche Ausnahme ein Block. Ein solcher Block beginnt mit dem catch-Schüsselwort, danach folgen in runden Klammern der Name der erwarteten Exception-Klasse und der Name einer Variablen, in der das entsprechende Objekt im catch-Block bereitgestellt werden soll. Der eigentliche catch-Block folgt direkt im Anschluss und ist analog zum try-Block mit geschweiften Klammern umschlossen. Beispiel zeigt die Verwendung von try und catch.

Beispiel: Verwendung der um Ausnahmebehandlung erweiterten Klasse

  1. <?php
  2. require_once 'DB_MySQL.php';
  3.  
  4. try {
  5.   $mysql = new DB_MySQL(
  6.     'localhost',
  7.     'test',
  8.     'root',
  9.     'wrongPass'
  10.   );
  11.  
  12.   $mysql->query('SELECT spalte FROM tabelle');
  13.  
  14.   while ($row = $mysql->fetchRow()) {
  15.     // ...
  16.   }
  17. }
  18.  
  19. catch (DB_Exception $e) {
  20.   printf(
  21.     'Ein Datenbankfehler ist aufgetreten: %s',
  22.     $e->getMessage()
  23.   );
  24. }
  25.  
  26. catch (Exception $e) {
  27.   printf(
  28.     'Ein allgemeiner Fehler ist aufgetreten: %s',
  29.     $e->getMessage()
  30.   );
  31. }
  32. ?>

In anderen Programmiersprachen, die Ausnahmebehandlung anbieten (beispielsweise Java), kann nach einem catch-Block optional noch ein finally-Block folgen. Ein solcher Block wird auf jeden Fall ausgeführt. Eine typische Anwendung ist die Freigabe von Ressourcen oder das Schließen von Dateien. PHP bietet diesen Mechanismus in Version 5.0 noch nicht an, dieser kann jedoch nachgebildet werden.

Beispiel: Nachbildung von finally in PHP

  1. <?php
  2. class FinallyImplementierung {
  3.   public function methode() {
  4.     $ausnahme = NULL;
  5.  
  6.     try {
  7.       // Code, der eine Ausnahme auslösen kann.
  8.     }
  9.  
  10.     catch (Exception $e) {
  11.       // Ausnahme-Objekt speichern.
  12.       $ausnahme = $e;
  13.  
  14.       // Code, der eine Ausnahme behandelt.
  15.     }
  16.  
  17.     // finally {
  18.       // Code, der immer ausgeführt wird.
  19.     // }
  20.  
  21.     // Gespeichertes Ausnahme-Objekt weiterreichen.
  22.     if ($ausnahme !== NULL) {
  23.       throw $ausnahme;
  24.     }
  25.  
  26.     // Code, der nur ausgeführt wird,
  27.     // wenn keine Ausnahme ausgelöst wurde.
  28.   }
  29. }
  30. ?>

Eine unbehandelte Ausnahme führt zum Abbruch der Programmausführung. Hierbei wird ein so genannter Stack-Trace ausgegeben, der bei der Suche nach der Ursache der Ausnahme helfen soll.

Beispiel: Eine unbehandelte Ausnahme führt zum Abbruch der Programmausführung

  1. <?php
  2. throw new Exception('Test');
  3. ?>
Fatal error: Uncaught exception 'Exception' with message 'Test' in /home/sb/uncaught_exception.php:2
Stack trace:
#0 {main}
  thrown in /home/sb/uncaught_exception.php on line 2

Für die Verarbeitung von unbehandelten Ausnahmen kann eine PHP-Funktion mit der Funktion set_exception_handler() registriert werden. Auf diesem Weg kann die Ausnahme beispielsweise in einem Logfile protokolliert oder die Ausgabe der Fehlermeldung angepasst werden.

Beispiel: Anpassen der Verarbeitung von unbehandelten Ausnahmen

  1. <?php
  2. function my_exception_handler($e) {
  3.   // Exception $e behandeln.
  4. }
  5.  
  6. set_exception_handler('my_exception_handler');
  7. ?>

(Quelle: Fehlerbehandlung, 28.02.2008)

1.1  Richtlinen zur Fehlerbehandlung

2.  Was ist ein Fehler?

Ein Fehler ist definiert als ein unerwartet, nicht korrekter Programmzustande, der nicht wieder behoben werden kann. Zur Vereinfachung dieser Definition gilt, dass die Behebung des Fehlers nicht innerhalb einer Methode möglich ist. Eine unvollständige Behebung gilt trotzdem als Behebung.

Beispiel 4-1. Eine typische Fehlersituation

  1. <?php
  2. /**
  3. * Verbinde zur angegebenen Datenbank
  4. *
  5. * @throws Example_Datasource_Exception wenn der Verbindungsaufbau fehlschlägt
  6. */
  7. function connectDB($dsn) {
  8.     $this->db =& DB::connect($dsn);
  9.     if (DB::isError($this->db))
  10.     {
  11.         throw new Example_Datasource_Exception(
  12.                 "Unable to connect to $dsn:" . $this->db->getMessage()
  13.         );
  14.     }
  15. }
  16. ?>

In diesem Beispiel soll die Methode die Verbindung mit der Datenbank mit dem gegegebenem DSN herstellen. Die Methode ruft ihrerseits nur DB, wenn dieses Package einen Fehler wirft, kann nur eine Exception erzeugt und geworfen werden, ohne weiter Einfluß nehmen zu können.

Beispiel: Fehlerbehandlung mit Behebung

  1. <?php
  2. /*
  3. * Verbinde mit einer der möglichen Datenbanken
  4. *
  5. * @throws Example_Datasource_Exception wenn keine der gewählten
  6. *         Datenbank angesprochen werden konnte.
  7. *
  8. * @throws Example_Config_Exception wenn keine Datenbanken
  9. *         konfiguriert wurden
  10. */
  11.  
  12. function connect(Config $conf)
  13. {
  14.     $dsns =& $conf->searchPath(array('config', 'db'));
  15.     if ($dsns === FALSE) throw new Example_Config_Exception(
  16.         'Unable to find config/db section in configuration.'
  17.     );
  18.  
  19.     $dsns =& $dsns->toArray();
  20.  
  21.     foreach($dsns as $dsn) {
  22.         try {
  23.             $this->connectDB($dsn);
  24.             return;
  25.         } catch (Example_Datasource_Exception e) {
  26.             // Warn-/Logging-Code um den Verbindungsfehler
  27.             // aufzuzeichnen
  28.         }
  29.     }
  30.     throw new Example_Datasource_Exception(
  31.         'Unable to connect to any of the configured databases'
  32.     );
  33. }
  34. ?>

Das zweite Beispiel zeigt, wie eine Exception empfangen und behandelt wird. Die verwendete connectDB()-Methode kann nur einen Fehler melden, wenn die Verbindung fehlschlägt. Die übergeordnete Methode connect() hingegen weiss, dass das Objekt auch mit einer der anderen Datenbank-Verbindungen lauffähig ist. Deshalb kann der Fehler als behoben angesehen werden und die Exception wird nicht weitergeleitet.

Beispiel: Unvollständige Behebung

  1. <?php
  2. /**
  3. * loadConfig wertet die angegebene Konfiguration aus. Wenn die
  4. * Konfiguration unkorrekt ist, dann wird auf die Standard-
  5. * zurückgegriffen
  6. *
  7. */
  8. function loadConfig(Config $conf)
  9. {
  10.     try {
  11.         $this->config = $conf->parse();
  12.     } catch (Config_Parse_Exception e) {
  13.         // Warn-/Logging-Code
  14.         // Unvollständige Fehlerbehebung
  15.         $this->config = $this->defaultConfig;
  16.     }
  17. }
  18. ?>

Die Fehlerbehebung führt zu Seiteneffekten, deshalb ist sie nicht vollständig. Das Programm kann weiterlaufen, die Exception gilt als behandelt und muss nicht weitergeleitet werden. Wie im vorherigen Beispiel sollte die aufgetretene Exception aber trotzdem geloggt werden oder eine andere Form der Warnung stattfinden.

3.  Fehler-Benachrichtigung

Fehlerhafte Zustände müssen über Exceptions gemeldet werden. Nicht mehr verwendet werden sollten Fehlercodes oder ein Error-Objekt. Diese Regel gilt natürlich nicht, wenn das Package kompatibel mit PHP 4 bleiben muss.

Eine Exception sollte immer geworfen werden, wenn ein fehlerhafter Zustand auftritt, entsprechend der Definition im vorherigen Abschnitt. Die geworfene Exception sollte genügend Informationen enthalten, um den Fehler debuggen zu können und schnell dessen Grund herauszufinden. Bedenken Sie, dass in Produktionsumgebungen keine Exception an den Endanwender durchdringen sollten. Deshalb muss man sich keine Gedanken machen über die Komplexität der Fehlermeldung.

Die Basis-Klasse Exception enthält eine wörtliche Beschreibung des Fehlers, womit der Programmzustand beschrieben wird, der zum Fehler führte, und - optional - Execptions, die durch untergeordnete Programmaufrufe herbei geführten wurden und die ursprüngliche Ursache des Fehlers darstellen können.

Die Arten von Informationen die in einer Exception enthalten sein müssen, hängt von der Art des Fehlers ab. Es gibt drei Varianten von Exceptions:

  1. Fehler, die während der Vorabprüfung auftreten können.
  2. Fehler, die durch untergeordneten Bibliotheksaufrufe auftreten und durch Fehlercodes oder -Objekte signalisiert werden
  3. Nicht-korrigierbare Exceptions von untergeordneten Bibliotheken.

Fehler, die während der Vorabprüfung auftreten können, sollten eine Beschreibung der fehlgeschlagenen Prüfung enthalten. Wenn möglich sollte der fehlerhafte Wert mit angegeben werden.

Beispiel

  1. <?php
  2. function divide($x, $y)
  3. {
  4.     if ($y == 0) {
  5.         throw new Example_Aritmetic_Exception('Division by zero');
  6.     }
  7. }
  8. ?>

Fehler, die durch untergeordneten Bibliotheksaufrufe auftreten und durch Fehlercodes oder -Objekte signalisiert werden, sollten in Exceptions umgewandelt werden, wenn diese nicht behoben werden können. Die Fehlerbeschreibung sollte die originale Fehlerinformationen enthalten bzw. entsprechend konvertiert werden. Am Beispiel der obigen connect()-Methode:

Beispiel

  1. <?php
  2. /**
  3. * Verbinde zur angegebenen Datenbank
  4. *
  5. * @throws Example_Datasource_Exception wenn der Verbindungsaufbau fehlschlägt
  6. */
  7. function connectDB($dsn) {
  8.     $this->db =& DB::connect($dsn);
  9.     if (DB::isError($this->db)) {
  10.         throw new Example_Datasource_Exception(
  11.                 "Unable to connect to $dsn:" . $this->db->getMessage()
  12.         );
  13.     }
  14. }
  15. ?>

Nicht-korrigierbare Exceptions von untergeordneten Bibliotheken sollten weitergeleitet oder erneut geworfen werden. Wenn sie weitergeleitet werden soll, dann behandeln Sie die Exception nicht weiter. Wenn Sie die Exception erneut werfen, dann müssen Sie die originale Exception in der neuen Exception verpacken.

Beispiel: Eine Exception neu verpacken

  1. <?php
  2. function preTaxPrice($retailPrice, $taxRate)
  3. {
  4.     try {
  5.         return $this->divide($retailPrice, 1 + $taxRate);
  6.     } catch (Example_Aritmetic_Exception e) {
  7.         throw new Example_Tax_Exception('Invalid tax rate.', e);
  8.     }
  9. }
  10. ?>

Beispiel: Eine Exception weiterleiten

  1. <?php
  2. function preTaxPrice($retailPrice, $taxRate)
  3. {
  4.     return $this->divide($retailPrice, 1 + $taxRate);
  5. }
  6. ?>

Die Entscheidung, ob eine Exception neu verpackt oder weitergeleitet werden soll, ist eine Frage der Software-Architektur. Exceptions sollten weitergeleitet werden, ausser in zwei Fällen:

  1. Die originale Excpetion ist von einem anderen Package. Wenn diese weitergeleitet wird, dann würden Details der Implementierung nach aussen dringen.
  2. Die Methode kann nützliche Debug-Informationen ergänzen.

4.  Exceptions und der normale Programmfluß

Exceptions sollten niemals als Bestandteil des normalen Programmflußes benutzt werden. Wenn alle Logik zur Behandlung von Exceptions entfernt würde (try-catch-Statements), dann sollte der verbliebende Code den "wahren Pfad" repräsentieren -- dem Programmfluß, wenn keinerlei Fehler auftreten würden.

Diese Forderung entspricht der Erwartung, dass Exceptions nur bei fehlerhaften Zuständen geworfen werden sollten und niemals bei regulären Zuständen.

Ein Beispiel für die falsche Benutzung der Exception-Weiterleitung ist die Rückgabe eines Wertes eines rekursiven Aufrufs:

Beispiel

  1. <?php
  2. /**
  3. * Rekursive Suche in einem Baum nach einem String
  4. * @throws ResultException
  5. */
  6. public function search(TreeNode $node, $data) 
  7. {
  8.     if ($node->data === $data) {
  9.          throw new ResultException( $node );
  10.     } else {
  11.          search( $node->leftChild, $data );
  12.          search( $node->rightChild, $data );
  13.     }
  14. }
  15. ?>

Im Beispiel wird die ResultException benutzt, um "schnell" wieder aus der Rekursion heraus zu kommen. Das ist im Fehlerfall tatsächlich praktisch, in diesem Fall, aber nur ein Beispiel für einen faulen Programmierer.

5.  Die Klassen-Hierarchie von Exceptions

Alle Exceptions, die von Packages geworfen werden, müssen von Exception abstammen.

Zusätzlich sollte jedes Package seine eigene Exception-Klasse definieren; der Name der Klasse entspricht dem Muster: <Package_Name>_Exception. Jede Exception sollte von dieser Klasse abgeleitet werden.

6.  Exceptions dokumentieren

Da PHP, im Gegensatz zu Java, es nicht erfordert, mögliche Exceptions in der Funktionssignatur aufzunehmen, ist deren sorgfältige Dokumentation im Methodenkopf wichtig.

Beispiel: Exceptions werden dokumentiert mit dem @throws-Schlüsselwort

  1. <?php
  2. /**
  3. * Diese Methode sucht nach Aliens.
  4. *
  5. * @return array Array von Alien-Objekten.
  6. * @throws AntennaBrokenException wenn die Impedanz-Leser anzeigt, dass die
  7. *         Antenne nicht funktioniert
  8. *
  9. * @throws AntennaInUseException wenn ein anderer Prozess die Antenne
  10. *         bereits benutzt
  11. */
  12. public function findAliens($color = 'green');
  13. ?>

In vielen Fällen wandelt die mittlere Schicht einer Anwendung Exceptions von untergeordneten Methoden in aussagekräftiger, anwendungsspezifische Exceptions. Das sollte ebenfalls angesprochen werden:

Beispiel

  1. <?php
  2. /**
  3. * Lade Session-Objekte in den Shared-Memory
  4. *
  5. * @throws LoadingException Jede untergeordnete IOException wird als
  6. *         LoadingException neu verpackt.
  7. */
  8. public function loadSessionObjects();
  9. ?>

In anderen Fällen kann ihre Methode als Filter fungieren, der nur bestimmte Exceptions weiterleitet. Dann sollten Sie dokumentieren, welche Exceptions nicht von Ihrer Methode abgefangen werden.

Beispiel

  1. <?php
  2. /**
  3. * Führt eine Reihe von Datenbankanfragen aus (atomar, nicht innerhalb einer Transaktion).
  4. * @throws SQLException Low-level SQL-Fehler werden direkt weitergeleitet.
  5. */
  6. public function batchExecute();
  7. ?>

7.  Exceptions als Teil der API

Exceptions spielen eine kritische Rolle in der API ihrer Bibliothek. Entwickler sollten angemessene Beschreibungen wo und warum Exceptions auftreten liefern. Auch die sorgfältige Planung der Fehlermeldungen ist ein wichtiger Faktor für die Erhaltung der Rückwärts-Kompatibilität.

Da Exceptions ein integraler Bestandteil der API ihres Packages sind, darf bei Änderungen daran die Rückwärts-Kompatibilität (BC) nicht grundlos gebrochen werden.

Dinge, die zum Bruch führen:

  • Jede Änderung an Methoden, die Exceptions werfen.
  • Wenn eine Exception-Klasse verwendet wird, die höher in der Vererbungskette liegt, als die ursprüngliche. Zum Beispiel, wenn Sie in einer neueren Version eine Exception werfen würden, in der alten aber z.B. IOException verwendet haben.

Dinge, die nicht zum Bruch führen:

  • Wenn eine abgeleitete Klasse der originalen Exception verwendet wird. Zum Beispiel, wenn Sie in der aktuellen Version eine IOException werfen, und in älteren Versionen Exception. Natürlich nur unter der Voraussetzung, dass IOException von Exception) abgeleitet ist.

(Quelle: Richtlinien, 28.02.2008)

Letzte Änderung am 22.08.2008 22:06 Uhr von chueser. navigation & toc