Informationsreihe PHP Exceptionhandling, Teil 1: Exceptions?

Da ich mir vorgenommen habe, mal wieder mehr in meinem Blog zu schreiben, um mein Wissen auch für mich später noch zugänglich zu speichern und der Menschheit vielleicht keinen kleinen Dienst zu erweisen ( Tongue oder auch nicht...) gibt es ab heute eine kleine Informationsreihe zum Exceptionhandling in PHP.

Ich wünsche euch viel Spaß beim Lesen und würde mich auch sehr über Rückmeldungen und Fragen freuen.

Euer Julian

Hier also Teil 1 mit einer Einführung und ersten Beispielen...

Was sind Exceptions und was soll ich damit?
Exceptions sind ein interessantes Konzept zur Fehlerbehandlung, das insbesondere in Java starke Verwendung findet (allerdings auch in anderen Sprachen wie JavaScript, Python, ... überall mit feinen Besonderheiten). In meinen Artikeln lehne ich mich teilweise kurz an Java an, da PHP bekanntlich einiges in der OO von Java abkupfert.

Vor PHP5 kannte man in PHP das Konzept von Exceptions nicht, sondern benutzte die Funktion  trigger_error() , sowie eigene Rückgabewerte (arrays, false, 0, ...) zur Fehlersignalisierung.

Um Fehler zu unterdrücken, wurde den entsprechenden Funktionen ein "@" vorangestellt, welches zur Unterdrückung der Fehler führte.
Hierbei handelt es sich zwar um eine vielleicht im ersten Moment "nette" Idee, um Fehler schnell unsichtbar zu machen, letztendlich werden eigentlich gute Hinweise so aber nur versteckt und das Problem nicht gelöst. Ein ähnliches "Anti-Pattern" finden wir auch später bei Exceptions wieder. Mist machen kann man schließlich überall, manchmal braucht man es aber tatsächlich.

Nungut, zurück zum Thema.
Problem bei diesen trigger_error-Fehlermeldungen ist leider, dass entweder ein Fehler vorliegt, oder nicht... wir aber im Code nicht darauf reagieren können und diesen eventuell doch noch automatisch beheben.

Hier kommen Exceptions ins Spiel!

Exceptions werden per  throw new Exception('Meldung', 'Code'); geworfen.
Wie sich anhand der Syntax bereits erahnen lässt, handelt es sich bei einer Exception um ein Objekt. Das ist auch gut so, denn Objekte können Nachrichten empfangen und transportieren.

Stellen wir uns vor, unsere Anwendung ist eine Formularprüfung.
Nachdem der Benutzer ein Formular abgeschickt hat, wollen wir die einzelnen Felder auf korrekte Werte prüfen.

Wir durchlaufen die Felder also beispielsweise in einer Schleife.
Tritt ein Fehler auf, melden wir ihn beispielsweise per  "throw new Exception('Feld XYZ ist ein Pflichtfeld, wurde aber nicht ausgefüllt!');"
-- Wir gehen an dieser Stelle nicht auf noch besseren Einsatz von Objektorientierung, Exception-Chaining oder Mehrsprachigkeit ein --.

Zunächst einmal wäre unsere Anwendung damit schon ein Stück sicher, denn eine Exception führt ohne weitere Exceptionbehandlungen dazu, dass die Verarbeitung nach dem "Wurf" der Exception abbricht und der Benutzer die entsprechende Fehlermeldung (wie ein ERROR) erhält.

Soweit so gut, aber damit sind wir nicht zufrieden und bisher haben wir keine Vorteile gegenüber "trigger_error" erzielt.

Unser nächstes Ziel ist es, dem Benutzer eine annehmbare Ausgabe zu übermitteln und gleich alle Fehler anzuzeigen und nicht immer nur einen, wie es jetzt der Fall wäre.

Dazu setzen wir die Verarbeitung in einen try/catch-Block:

  1. $formularfelder = $this->getFormElements(); //Beispiel um Formularelemente-Array zu holen
  2. foreach($formularfelder as $feldObj){
  3. try{
  4.     $feldObj->evaluate(); //Überprüfungsmethode des Feldes, wirft eine Exception, wenn die Überprüfung nicht erfolgreich ist.
  5. } catch (Exception $e){
  6.     $fehlermeldungenObj->add($feldObj, $e); //Beispiel um Exception zu Feld zu merken
  7. }
  8. }

Bitte entschuldigt, wenn das Beispiel nicht 100% wasserdicht und sinnvoll modelliert ist, sicher werden einige kritisieren, dass man die Exceptionbehandlung beispielsweise auch im Feldobjekt machen könnte oder ähnliches. Hier gibt es viele Wege nach Rom mit Vor- und Nachteilen, aber hier geht es ja auch nicht um Softwarearchitektur.

Nungut, Ergebnis unserer Anpassung ist nun, dass unser Script in jedem Fall bis zum Ende durchläuft und nicht bei der ersten Exception abbricht (sofern nicht außerhalb unseres try/catch-Blockes auch noch ein Fehler passiert). Die auftretenden Fehler werden jetzt in unserem Fehlermeldungen-Objekt gespeichert und können später beispielsweise zentral in einer fest vorgesehenen Fehlermeldungs-Box ausgegeben werden.

Vorteil dieser Methode ist, dass wir so nichts am Feld-Objekt ändern müssen und dessen Funktionsweise nicht beeinflussen, dennoch Kontrolle über die Art der Fehlerbehandlung im Umgebenden Code haben.

Das hat insbesondere Vorteile, wenn fertige Bibliotheken eingesetzt werden, die wegen Updatesicherheit nicht manipuliert werden dürfen.

Als letzten Punkt für den heutigen Teil machen wir noch eine kleine Verbesserung an unserer Implementierung: Wir schaffen uns eigene Exceptiontypen, um darauf speziell reagieren zu können und diese unterschiedlich zu behandeln. Wir passen unsere Implementierung folgendermaßen an:

  1. $formularfelder = $this->getFormElements(); //Beispiel um Formularelemente-Array zu holen
  2. foreach($formularfelder as $feldObj){
  3.     try{
  4.         $feldObj->evaluate(); //Überprüfungsmethode des Feldes, wirft eine FormElementEvaluateException, wenn die Überprüfung nicht erfolgreich ist.
  5.     } catch (FormElementEvaluateException $e){
  6.         $fehlermeldungenObj->add($feldObj, $e); //Beispiel um FormElementEvaluateException zu Feld zu merken
  7.     }
  8. }

Die FormElementEvaluateException ist eine spezielle Exception, die vom generellen Exceptiontyp ableitet. Das kann mit einer folgenden Klassen-Implementierung erledigt werden:

  1. class FormElementEvaluateException extends Exception{}

Da wir das Standardverhalten der Exception nicht ändern wollen, müssen wir auch keine der geerbten Methoden anpassen. Diese werden allesamt vererbt.

Vorteil dieser Anpassung ist, dass wir nur auf (Eingabe-)Fehler im Formelement reagieren. Andere Fehler rauschen durch unsere Exceptionverarbeitung und führen zu einem Abbruch des Scriptes.
Das ist auch gut so, schließlich könnte ja ein schwerer Fehler aufgetreten sein, der nicht einfach nur in der Fehlerbox ausgegeben werden soll und an dieser Stelle (noch) garnicht behandelt werden kann.

An dieser Stelle sei noch angemerkt, dass dazu die Methode "evaluate()" selbstverständlich auch die Exception vom entsprechenden Typ im entsprechenden Fall werfen muss, wir können schließlich nicht zaubern.
Die Implementierung hier sähe also beispielsweise so aus:

  1. public function evaluate(){
  2. [...]
  3. if(trim($this->getValue) === '' and $this->isRequired()){
  4.     throw new FormElementEvaluateException($this->getName().' ist ein Pflichtfeld!');
  5. }
  6. [...]
  7. }

Auch hier könnte man darüber streiten, ob es nicht sogar sinnvoll wäre, eine FormElementRequiredException o.Ä. zu werfen, anstatt die Fehlermeldung im Text genauer zu beschreiben.
Zumindest beim Thema Mehrsprachigkeit hätten wir dadurch ggf. Vorteile.

In den nächsten Teilen geht es dann unter anderem um:

  • Einsatzbereiche von Exceptions
  • Error-Handler zur Umwandlung von ERRORs in Exceptions
  • SPL Exceptiontypen
  • Standardhandler für Exceptions
  • Exception-Chaining
  • Anti-Patterns der Fehlerbehandlung
  • Dokumentation von Exceptions
  • Exceptions aus Frameworks

Wenn ihr weitere Wünsche, Vorschläge oder konstruktive Kritik habt, immer her damit!

Comments

Post new comment

The content of this field is kept private and will not be shown publicly.

More information about formatting options