MWCodebymw.de ↗
PHP

Eigene Exceptions in PHP – Fehler, die man auseinanderhalten kann

catch (Exception $e) fängt alles – auch das, was du gar nicht behandeln wolltest. Mit ein paar eigenen Exception-Klassen wird aus „irgendwas ging schief" eine Fehlermeldung, mit der der Nutzer etwas anfangen kann.

Diese Zeile steht in erschreckend vielen PHP-Projekten:

try {
    $rechnung = erstelleRechnung($daten);
} catch (Exception $e) {
    echo 'Es ist ein Fehler aufgetreten.';
}

Das Problem ist nicht der try-Block, sondern dass hier alles in denselben Topf fällt: die fehlende Kundennummer, der abgestürzte Datenbankserver und der Tippfehler im Feldnamen. Der Nutzer bekommt denselben nichtssagenden Satz, und du beim Debuggen auch.

Ein paar eigene Klassen reichen

class RechnungsFehler extends RuntimeException {}

final class KundeFehlt extends RechnungsFehler {}

final class BetragUngueltig extends RechnungsFehler
{
    public function __construct(public readonly int $cent)
    {
        parent::__construct('Betrag ungültig: ' . $cent . ' Cent');
    }
}

Mehr ist das nicht. Eine leere Klasse, die von RuntimeException erbt, ist eine vollwertige Exception — und trägt jetzt eine Information im Namen.

Damit wird aus dem Sammel-catch etwas Nützliches:

try {
    $rechnung = erstelleRechnung($daten);
} catch (KundeFehlt) {
    fehlermeldung('Bitte wähle zuerst einen Kunden aus.');
} catch (BetragUngueltig $e) {
    fehlermeldung('Der Betrag ' . formatEur($e->cent) . ' ergibt keinen Sinn.');
} catch (PDOException $e) {
    log_fehler($e);
    fehlermeldung('Die Datenbank antwortet gerade nicht. Bitte später erneut versuchen.');
}

Drei Fälle, drei Reaktionen. Die ersten beiden kann der Nutzer selbst beheben — die dritte nicht, deshalb wird sie protokolliert statt erklärt.

Übrigens: Seit PHP 8 darfst du die Variable weglassen (catch (KundeFehlt)), wenn du sie gar nicht brauchst.

Die Ursache mitschleppen

Wenn du einen technischen Fehler in einen fachlichen übersetzt, wirf die Ursache nicht weg:

try {
    $db->prepare($sql)->execute($werte);
} catch (PDOException $e) {
    throw new RechnungsFehler('Rechnung konnte nicht gespeichert werden.', 0, $e);
}

Der dritte Parameter ist die vorherige Exception. Im Log siehst du dann beides: den verständlichen Satz und darunter die echte SQL-Meldung samt Zeilennummer. Ohne das steht im Log nur noch deine eigene Zusammenfassung, und die hilft beim Suchen selten weiter.

finally – das, was immer passieren muss

$fh = fopen($pfad, 'wb');
try {
    schreibeDaten($fh, $zeilen);
} catch (IOFehler $e) {
    protokolliere($e);
    throw $e;
} finally {
    fclose($fh);        // läuft in JEDEM Fall
}

finally läuft, egal wie der Block endet: normal, mit return, oder weil eine Exception nach oben durchgereicht wird. Genau dorthin gehört alles, was aufgeräumt werden muss — Dateien schließen, eine Transaktion zurückrollen, eine Sperre freigeben.

Meine Regel

Wirf eine Exception, wenn die Funktion ihre Aufgabe nicht erfüllen kann. Gib false oder null zurück, wenn „nichts gefunden" ein ganz normales Ergebnis ist. Ein kundeSuchen(), das bei „gibt es nicht" eine Exception wirft, macht jeden Aufrufer umständlicher — ein rechnungSpeichern(), das bei einem Fehler stillschweigend false zurückgibt, produziert dagegen Datenverlust, den niemand bemerkt.

Und: niemals einen leeren catch. Wenn du einen Fehler wirklich ignorieren willst, schreib wenigstens einen Kommentar dazu, warum das in Ordnung ist. In sechs Monaten weißt du es sonst nicht mehr.

Du hast ein PHP-Projekt, das bei Fehlern nur eine weiße Seite zeigt? Da lässt sich mit wenig Aufwand viel gewinnen. Schreib mir.

Quellen

#PHP#Exceptions#Fehlerbehandlung#finally#Klassen

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →