mitmario.dev

Eigene Ausnahmen

PHP Sandbox 3 Min Lesezeit 4 BeispieleLektion 3 von 6

Eine eigene Ausnahme ist in PHP erstaunlich billig: ein Name, das Wort extends, eine vorhandene Ausnahme, fertig. Kein Körper, keine Methoden, nichts.

Drei Zeilen, und du hast eine eigene Sorte

Eine eigene Ausnahme in drei Zeilen
<?php

class LagerLeerException extends RuntimeException
{
}

function nimmAusLager(int $vorrat, int $menge): int
{
    if ($menge > $vorrat) {
        throw new LagerLeerException("Nur noch " . $vorrat . " Stueck da");
    }

    return $vorrat - $menge;
}

foreach ([[10, 3], [7, 99]] as [$vorrat, $menge]) {
    try {
        $rest = nimmAusLager($vorrat, $menge);
        echo "Rest: ", $rest, "\n";
    } catch (LagerLeerException $fehler) {
        echo "Lager: ", $fehler->getMessage(), "\n";
    }
}

class LagerLeerException extends RuntimeException {} legt eine neue Ausnahme an, die alles kann, was RuntimeException kann, und sich nur durch ihren Namen unterscheidet. Genau darum geht es.

Eine Klasse kennst du aus Lektion 6.7, und das hier ist die kürzeste, die es überhaupt gibt: ein Name, ein extends, und zwischen den geschweiften Klammern steht nichts. Sie erbt alles, was sie braucht. Was man sonst noch mit einer Klasse anfängt, steht in Abschnitt 13; für diese Lektion reicht dir: Das ist der Weg, eine neue Ausnahme-Sorte zu erfinden.

Die Endung Exception im Namen ist Gewohnheit, kein Zwang. Halt dich trotzdem daran, dein späteres Ich sucht danach.

Der eigentliche Gewinn: der Aufrufer kann unterscheiden

Zwei Fälle, zwei catch-Blöcke
<?php

class MengeFehltException extends InvalidArgumentException
{
}

class PreisUngueltigException extends InvalidArgumentException
{
}

function pruefe(array $posten): void
{
    if (!isset($posten["menge"])) {
        throw new MengeFehltException("Menge fehlt");
    }

    if (!is_numeric($posten["preis"])) {
        throw new PreisUngueltigException("Preis ist keine Zahl");
    }
}

$bestellung = [
    ["menge" => 2, "preis" => "9.90"],
    ["preis" => "4.50"],
    ["menge" => 1, "preis" => "vier Euro"],
];

foreach ($bestellung as $posten) {
    try {
        pruefe($posten);
        echo "In Ordnung\n";
    } catch (MengeFehltException $fehler) {
        echo "Nachfragen: ", $fehler->getMessage(), "\n";
    } catch (PreisUngueltigException $fehler) {
        echo "Ablehnen: ", $fehler->getMessage(), "\n";
    }
}

Hier stehen zwei Ausnahmen für zwei verschiedene Probleme, und der Aufrufer behandelt sie unterschiedlich: bei einer fehlenden Menge fragt er nach, bei einem kaputten Preis lehnt er ab.

Man könnte das auch mit einer einzigen Ausnahme und zwei verschiedenen Meldungen bauen. Dann müsste der catch-Block aber in den Text hineinsehen, etwa mit str_contains($fehler->getMessage(), "Menge"). Und damit hängt dein Programm an einem Wortlaut. Ändert jemand die Meldung, oder wird sie irgendwann übersetzt, geht der Zweig still kaputt.

Der Typ ist die verlässliche Angabe, der Text ist für Menschen. Das ist der ganze Grund für eigene Ausnahmen, und er reicht auch schon aus.

Ein try darf beliebig viele catch-Blöcke haben. Passt keiner, fliegt die Ausnahme weiter nach oben, als stünde das try gar nicht da.

Die Reihenfolge zählt, und niemand warnt dich

Von speziell nach allgemein
<?php

class MengeFehltException extends InvalidArgumentException
{
}

echo "Richtig herum, speziell zuerst:\n";
try {
    throw new MengeFehltException("Menge fehlt");
} catch (MengeFehltException $fehler) {
    echo "  MengeFehltException\n";
} catch (InvalidArgumentException $fehler) {
    echo "  InvalidArgumentException\n";
}

echo "Falsch herum, allgemein zuerst:\n";
try {
    throw new MengeFehltException("Menge fehlt");
} catch (InvalidArgumentException $fehler) {
    echo "  InvalidArgumentException, und zwar ein ", get_class($fehler), "\n";
} catch (MengeFehltException $fehler) {
    echo "  Diese Zeile ist nicht zu erreichen\n";
}

Beide Ausnahmen im Beispiel sind eine MengeFehltException, und beide try-Blöcke haben dieselben zwei catch-Blöcke. Nur die Reihenfolge ist vertauscht, und das Ergebnis ist verschieden.

PHP nimmt den ersten catch-Block, der passt. Und weil MengeFehltException von InvalidArgumentException erbt, passt der allgemeine Block auch auf die spezielle Ausnahme. Steht er oben, fängt er alles, und der speziellere darunter wird nie erreicht.

Das Unangenehme daran: Es ist kein Fehler. Keine Meldung, keine Warnung, der Exit-Code bleibt 0. Dein Programm behandelt einfach alles gleich, und du merkst es an der falschen Ausgabe. Genau die Sorte Problem aus Lektion 7.1. Im Reiter Debug kannst du es dir vorführen lassen: Die Zeile mit Diese Zeile ist nicht zu erreichen taucht in der Aufzeichnung nie auf, und fehler heißt im allgemeinen catch-Block trotzdem MengeFehltException.

Die Regel dagegen ist kurz: von speziell nach allgemein. Der engste Typ steht oben, der weiteste unten. Wenn du unsicher bist, welcher enger ist: Der, der extends sagt, ist der engere.

Wenn zwei Fälle doch dasselbe bekommen sollen

Mehrere Sorten in einem catch
<?php

class MengeFehltException extends InvalidArgumentException
{
}

class PreisUngueltigException extends InvalidArgumentException
{
}

foreach (["menge", "preis"] as $fall) {
    try {
        if ($fall === "menge") {
            throw new MengeFehltException("Menge fehlt");
        }
        throw new PreisUngueltigException("Preis ist keine Zahl");
    } catch (MengeFehltException | PreisUngueltigException $fehler) {
        echo "Posten abgelehnt: ", $fehler->getMessage(), "\n";
    }
}

Manchmal willst du unterscheiden können, tust es an dieser Stelle aber nicht. Dafür gibt es den senkrechten Strich: catch (MengeFehltException | PreisUngueltigException $fehler) fängt beide und nur diese beiden.

Das ist besser als ein catch (InvalidArgumentException $fehler), obwohl es hier dasselbe täte. Der Strich sagt, welche zwei Fälle du erwartest. Kommt später eine dritte Sorte dazu, fliegt sie durch und fällt auf, statt still mitbehandelt zu werden.

Wann eine eigene Ausnahme übertrieben ist. Wenn es nur einen Fall gibt und niemand ihn von einem anderen unterscheiden muss, reicht eine RuntimeException mit einer guten Meldung. Eine eigene Sorte lohnt sich ab dem Moment, in dem irgendwo ein zweiter catch-Block danebenstehen könnte.

Zum Mitnehmen

Bisher hast du gefangen, was PHP dir hingeworfen hat. Jetzt legst du eigene Sorten an, und der Gewinn ist größer, als er aussieht: Der Aufrufer kann danach auf den Fall reagieren, statt in einer Meldung nach Stichwörtern zu suchen.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.