18 Fehlerbehandlung und Exceptions

Die effektive Fehlerbehandlung ist ein entscheidender Aspekt robuster PHP-Anwendungen. PHP bietet verschiedene Mechanismen zur Fehlerbehandlung, wobei Exceptions eine zentrale Rolle in der modernen objektorientierten Programmierung spielen. In diesem Abschnitt werden wir die verschiedenen Arten von Fehlern in PHP, die Verwendung von Exceptions und bewährte Praktiken für die Fehlerbehandlung behandeln.

18.1 Fehlerarten in PHP

PHP unterscheidet zwischen verschiedenen Arten von Fehlern:

  1. Parsefehler (Parse Errors): Syntaxfehler, die das Parsen des Codes verhindern
  2. Fatale Fehler (Fatal Errors): Schwerwiegende Laufzeitfehler, die die Ausführung stoppen
  3. Warnungen (Warnings): Nicht-fatale Laufzeitfehler
  4. Hinweise (Notices): Laufzeithinweise auf mögliche Probleme
  5. Deprecated-Hinweise: Warnungen über veraltete Funktionen oder Features
  6. Strict-Standards-Hinweise: Hinweise auf empfohlene Codierungspraktiken
  7. Exceptions: Objektorientierter Mechanismus zur Fehlerbehandlung

18.2 Traditionelle Fehlerbehandlung in PHP

Traditionell verwendet PHP Funktionen wie error_reporting(), set_error_handler() und trigger_error() zur Fehlerbehandlung:

// Fehlerreporting-Level einstellen (alle Fehler außer Notices und Strict)
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);

// Eigenen Error-Handler definieren
function meinErrorHandler(int $errno, string $errstr, string $errfile, int $errline): bool {
    echo "Fehler [$errno] $errstr - Zeile $errline in Datei $errfile\n";
    
    // Rückgabe false bedeutet, dass der Standard-Error-Handler ebenfalls ausgeführt wird
    // Rückgabe true bedeutet, dass der Fehler als behandelt gilt
    return true;
}

// Error-Handler registrieren
set_error_handler('meinErrorHandler');

// Manuell einen Fehler auslösen
trigger_error("Dies ist ein benutzerdefinierter Fehler", E_USER_WARNING);

// Undefined variable (würde normalerweise eine Notice auslösen)
echo $undefinierteVariable;

Diese Methode hat jedoch Einschränkungen, insbesondere im Kontext objektorientierter Programme, weshalb der Einsatz von Exceptions empfohlen wird.

18.3 Grundlagen der Exception-Behandlung

Exceptions bieten einen strukturierten, objektorientierten Ansatz zur Fehlerbehandlung:

try {
    // Code, der möglicherweise eine Exception auslöst
    $wert = 10 / 0; // Division durch Null
} catch (DivisionByZeroError $e) {
    // Behandlung spezifischer Exceptions
    echo "Fehler bei der Division: " . $e->getMessage();
} catch (Exception $e) {
    // Fallback für andere Exceptions
    echo "Allgemeiner Fehler: " . $e->getMessage();
} finally {
    // Wird immer ausgeführt, unabhängig davon, ob eine Exception auftrat
    echo "Aufräumarbeiten werden durchgeführt...";
}

Der try-Block enthält Code, der möglicherweise Exceptions auslöst. Der catch-Block fängt spezifische Exceptions ab und behandelt sie. Der optionale finally-Block enthält Code, der unabhängig vom Auftreten einer Exception ausgeführt wird.

18.4 Die Exception-Hierarchie in PHP

PHP verfügt über eine umfangreiche Hierarchie von eingebauten Exception-Klassen:

Exception (Basis-Exception-Klasse)
  ├── ErrorException (Wrapper für traditionelle PHP-Fehler)
  ├── Error (Basis für interne PHP-Fehler seit PHP 7)
  │     ├── ParseError
  │     ├── TypeError
  │     ├── ArgumentCountError
  │     ├── ArithmeticError
  │     │     ├── DivisionByZeroError
  │     └── CompileError
  │           └── CompileWarning
  ├── LogicException (Programmlogikfehler)
  │     ├── BadFunctionCallException
  │     │     └── BadMethodCallException
  │     ├── DomainException
  │     ├── InvalidArgumentException
  │     ├── LengthException
  │     └── OutOfRangeException
  └── RuntimeException (Laufzeitfehler)
        ├── OutOfBoundsException
        ├── OverflowException
        ├── RangeException
        ├── UnderflowException
        └── UnexpectedValueException

Diese Hierarchie ermöglicht eine präzise Fehlerbehandlung durch das Abfangen spezifischer Exception-Typen.

18.5 Eigene Exception-Klassen erstellen

Für anwendungsspezifische Fehler ist es oft sinnvoll, eigene Exception-Klassen zu definieren:

class DatenbankException extends Exception {
    protected $sqlAbfrage;
    
    public function __construct(string $nachricht, string $sqlAbfrage = "", int $code = 0, ?Throwable $previous = null) {
        parent::__construct($nachricht, $code, $previous);
        $this->sqlAbfrage = $sqlAbfrage;
    }
    
    public function getSqlAbfrage(): string {
        return $this->sqlAbfrage;
    }
}

class BenutzerNichtGefundenException extends DatenbankException {
    protected $benutzerId;
    
    public function __construct(int $benutzerId, string $sqlAbfrage = "", int $code = 0, ?Throwable $previous = null) {
        parent::__construct("Benutzer mit ID $benutzerId nicht gefunden", $sqlAbfrage, $code, $previous);
        $this->benutzerId = $benutzerId;
    }
    
    public function getBenutzerId(): int {
        return $this->benutzerId;
    }
}

// Verwendung
function benutzerLaden(int $id): array {
    // Simulierte Datenbankabfrage
    $sql = "SELECT * FROM benutzer WHERE id = $id";
    
    // Benutzer nicht gefunden (simuliert)
    if ($id > 1000) {
        throw new BenutzerNichtGefundenException($id, $sql);
    }
    
    // Datenbankfehler (simuliert)
    if ($id < 0) {
        throw new DatenbankException("Fehler bei der Datenbankverbindung", $sql);
    }
    
    // Erfolgreicher Fall (simuliert)
    return [
        'id' => $id,
        'name' => "Benutzer $id",
        'email' => "benutzer$id@example.com"
    ];
}

// Verwenden der Exceptions
try {
    $benutzer = benutzerLaden(1001); // Würde BenutzerNichtGefundenException auslösen
    echo "Benutzer: " . $benutzer['name'];
} catch (BenutzerNichtGefundenException $e) {
    echo "Fehler: " . $e->getMessage() . "\n";
    echo "Benutzer-ID: " . $e->getBenutzerId() . "\n";
    echo "SQL-Abfrage: " . $e->getSqlAbfrage() . "\n";
} catch (DatenbankException $e) {
    echo "Datenbankfehler: " . $e->getMessage() . "\n";
    echo "SQL-Abfrage: " . $e->getSqlAbfrage() . "\n";
} catch (Exception $e) {
    echo "Allgemeiner Fehler: " . $e->getMessage() . "\n";
}

Durch die Erstellung spezifischer Exception-Klassen können anwendungsspezifische Informationen und Verhaltensweisen gekapselt werden.

18.6 Das Throwable-Interface

Seit PHP 7.0 implementieren sowohl Exception als auch Error das Throwable-Interface, das die gemeinsame Basis für alle Fehlertypen in PHP bildet:

try {
    // Code, der entweder Exception oder Error auslösen könnte
    $obj = null;
    $obj->methode(); // Würde einen Error auslösen (Null-Dereferenzierung)
} catch (Throwable $t) {
    // Fängt jede Art von Exception oder Error
    echo "Gefangen: " . $t->getMessage() . "\n";
    echo "In Datei: " . $t->getFile() . " Zeile " . $t->getLine() . "\n";
}

Beachten Sie, dass man das Throwable-Interface nicht direkt implementieren kann. Eigene Exception-Klassen müssen entweder von Exception oder Error erben.

18.7 Exceptions werfen und weitergeben

Exceptions können an beliebiger Stelle im Code geworfen werden:

function validiereBenutzername(string $benutzername): void {
    $minLaenge = 3;
    $maxLaenge = 20;
    
    if (strlen($benutzername) < $minLaenge) {
        throw new InvalidArgumentException(
            "Benutzername zu kurz (mindestens $minLaenge Zeichen erforderlich)"
        );
    }
    
    if (strlen($benutzername) > $maxLaenge) {
        throw new InvalidArgumentException(
            "Benutzername zu lang (maximal $maxLaenge Zeichen erlaubt)"
        );
    }
    
    if (!preg_match('/^[a-zA-Z0-9_]+$/', $benutzername)) {
        throw new InvalidArgumentException(
            "Benutzername enthält ungültige Zeichen (nur Buchstaben, Zahlen und Unterstriche)"
        );
    }
}

function benutzerRegistrieren(string $benutzername, string $passwort): void {
    try {
        validiereBenutzername($benutzername);
        // Weitere Validierungen...
    } catch (InvalidArgumentException $e) {
        // Exception mit zusätzlichen Informationen neu werfen
        throw new RuntimeException(
            "Registrierung fehlgeschlagen: " . $e->getMessage(),
            0,
            $e // Original-Exception als "previous" speichern
        );
    }
    
    // Benutzer in Datenbank speichern...
    echo "Benutzer $benutzername erfolgreich registriert";
}

// Verwendung
try {
    benutzerRegistrieren("u$er", "passwort123");
} catch (Exception $e) {
    echo "Fehler: " . $e->getMessage() . "\n";
    
    // Zugriff auf die vorherige Exception (wenn vorhanden)
    if ($e->getPrevious()) {
        echo "Ursprünglicher Fehler: " . $e->getPrevious()->getMessage() . "\n";
    }
}

Die previous-Exception ermöglicht es, eine Kette von Exceptions zu verfolgen, was für das Debugging komplexer Anwendungen hilfreich ist.

18.8 Multi-Catch-Blöcke

Seit PHP 7.1 können mehrere Exception-Typen in einem einzigen Catch-Block behandelt werden:

try {
    // Code, der verschiedene Exceptions auslösen könnte
} catch (InvalidArgumentException | LengthException $e) {
    // Behandelt beide Exception-Typen gleich
    echo "Validierungsfehler: " . $e->getMessage();
} catch (RuntimeException $e) {
    // Spezifische Behandlung für RuntimeException
    echo "Laufzeitfehler: " . $e->getMessage();
} catch (Exception $e) {
    // Fallback für alle anderen Exception-Typen
    echo "Allgemeiner Fehler: " . $e->getMessage();
}

Diese Syntax reduziert doppelten Code, wenn mehrere Exception-Typen auf die gleiche Weise behandelt werden sollen.

18.9 Nested Exceptions und Exception-Chaining

Exceptions können geschachtelt werden, um eine Fehlerursachenkette zu erstellen:

function datenbankAbfrage(string $sql): array {
    try {
        // Simuliere eine Verbindungsherstellung
        if (rand(0, 10) === 0) {
            throw new RuntimeException("Datenbankverbindung nicht möglich");
        }
        
        // Simuliere eine Abfrageausführung
        if (stripos($sql, "SELECT") !== 0) {
            throw new InvalidArgumentException("Nur SELECT-Abfragen sind erlaubt");
        }
        
        // Simuliere Ergebnisse
        return [
            ['id' => 1, 'name' => 'Max Mustermann'],
            ['id' => 2, 'name' => 'Erika Musterfrau']
        ];
    } catch (Exception $e) {
        // Neue Exception werfen mit der ursprünglichen als Ursache
        throw new DatenbankException(
            "Fehler bei der Ausführung der Abfrage: " . $e->getMessage(),
            $sql,
            0,
            $e
        );
    }
}

try {
    $ergebnisse = datenbankAbfrage("INSERT INTO benutzer VALUES (...)");
} catch (DatenbankException $e) {
    echo "Datenbankfehler: " . $e->getMessage() . "\n";
    echo "SQL: " . $e->getSqlAbfrage() . "\n";
    
    $previous = $e->getPrevious();
    if ($previous) {
        echo "Ursache: " . get_class($previous) . ": " . $previous->getMessage() . "\n";
    }
    
    // Stack-Trace ausgeben
    echo "\nStack-Trace:\n" . $e->getTraceAsString() . "\n";
}

Mit dem Exception-Chaining kann die Ursache eines Fehlers durch mehrere Programmebenen verfolgt werden, was das Debugging erleichtert.

18.10 Verwenden von finally

Der finally-Block wird immer ausgeführt, unabhängig davon, ob eine Exception auftritt oder nicht. Dies ist besonders nützlich für Aufräumoperationen:

function dateiVerarbeiten(string $pfad): string {
    $handle = null;
    
    try {
        $handle = fopen($pfad, 'r');
        if ($handle === false) {
            throw new RuntimeException("Konnte Datei nicht öffnen: $pfad");
        }
        
        $inhalt = fread($handle, filesize($pfad));
        if ($inhalt === false) {
            throw new RuntimeException("Fehler beim Lesen der Datei: $pfad");
        }
        
        return $inhalt;
    } catch (Exception $e) {
        echo "Fehler bei der Dateiverarbeitung: " . $e->getMessage() . "\n";
        throw $e; // Rethrow der Exception nach der Protokollierung
    } finally {
        // Wird immer ausgeführt, auch wenn eine Exception auftritt
        if ($handle !== null) {
            echo "Datei wird geschlossen\n";
            fclose($handle);
        }
    }
}

try {
    $inhalt = dateiVerarbeiten('nicht-existierende-datei.txt');
    echo "Dateiinhalt: $inhalt";
} catch (Exception $e) {
    echo "Hauptprogramm: " . $e->getMessage();
}

Der finally-Block stellt sicher, dass Ressourcen ordnungsgemäß freigegeben werden, selbst wenn Exceptions auftreten.

18.11 Globale Exception-Handler

Für nicht abgefangene Exceptions kann ein globaler Exception-Handler registriert werden:

function globaleExceptionBehandlung(Throwable $exception): void {
    // Protokollierung des Fehlers
    error_log(sprintf(
        "Unbehandelte Exception: %s in %s Zeile %d\nStack-Trace:\n%s",
        $exception->getMessage(),
        $exception->getFile(),
        $exception->getLine(),
        $exception->getTraceAsString()
    ));
    
    // Benutzerfreundliche Fehlerseite anzeigen
    if (php_sapi_name() !== 'cli') {
        http_response_code(500);
        echo "<h1>Es ist ein Fehler aufgetreten</h1>";
        echo "<p>Wir entschuldigen uns für die Unannehmlichkeiten. Unser Team wurde informiert.</p>";
        
        // Im Entwicklungsmodus kann der Fehler angezeigt werden
        if (getenv('APP_ENV') === 'development') {
            echo "<h2>Fehlerdetails (nur im Entwicklungsmodus sichtbar):</h2>";
            echo "<pre>" . htmlspecialchars($exception) . "</pre>";
        }
    } else {
        echo "Fehler: " . $exception->getMessage() . "\n";
    }
}

// Globalen Exception-Handler registrieren
set_exception_handler('globaleExceptionBehandlung');

// Diese Exception wird vom globalen Handler abgefangen
throw new RuntimeException("Dies ist ein unbehandelter Fehler");

Der globale Exception-Handler dient als Fallback für unerwartete Fehler und kann für Protokollierung, Fehlerbenachrichtigungen und benutzerfreundliche Fehlermeldungen verwendet werden.

18.12 Konvertierung von PHP-Fehlern in Exceptions

Traditionelle PHP-Fehler können in Exceptions umgewandelt werden:

function fehlerZuExceptionHandler(int $severity, string $message, string $file, int $line): bool {
    // Fehler in Exception umwandeln, außer für Hinweise und andere niedrig eingestufte Fehler
    if (!(error_reporting() & $severity)) {
        return false; // Dieser Fehlertyp ist deaktiviert
    }
    
    throw new ErrorException($message, 0, $severity, $file, $line);
}

// Error-Handler registrieren
set_error_handler('fehlerZuExceptionHandler');

try {
    // Dies würde normalerweise eine E_WARNING auslösen
    $inhalt = file_get_contents('nicht-existierende-datei.txt');
    echo $inhalt;
} catch (ErrorException $e) {
    echo "Fehler abgefangen: " . $e->getMessage() . "\n";
    echo "Fehlertyp: " . $e->getSeverity() . "\n";
}

// Error-Handler wiederherstellen
restore_error_handler();

Diese Technik ermöglicht es, traditionelle PHP-Fehler mit dem Exception-System zu behandeln, was zu einem einheitlicheren Fehlerbehandlungsansatz führt.

18.13 Praktisches Beispiel: Fehlerbehandlung in einer Web-API

Hier ist ein Beispiel für die Implementierung einer umfassenden Fehlerbehandlung in einer REST-API:

// Fehlerbehandlungsklassen für eine API
abstract class ApiException extends Exception {
    protected $statusCode = 500;
    
    public function getStatusCode(): int {
        return $this->statusCode;
    }
    
    public function getResponseData(): array {
        return [
            'error' => [
                'code' => $this->getCode() ?: $this->getStatusCode(),
                'message' => $this->getMessage(),
                'details' => $this->getDetails()
            ]
        ];
    }
    
    protected function getDetails(): array {
        return [];
    }
}

class ValidationException extends ApiException {
    protected $statusCode = 400;
    protected $fehler = [];
    
    public function __construct(array $fehler, string $message = "Validierungsfehler", int $code = 0, ?Throwable $previous = null) {
        parent::__construct($message, $code, $previous);
        $this->fehler = $fehler;
    }
    
    protected function getDetails(): array {
        return ['validationErrors' => $this->fehler];
    }
}

class NotFoundException extends ApiException {
    protected $statusCode = 404;
    protected $resourceTyp;
    protected $resourceId;
    
    public function __construct(string $resourceTyp, $resourceId, string $message = "", int $code = 0, ?Throwable $previous = null) {
        $message = $message ?: "$resourceTyp mit ID $resourceId nicht gefunden";
        parent::__construct($message, $code, $previous);
        $this->resourceTyp = $resourceTyp;
        $this->resourceId = $resourceId;
    }
    
    protected function getDetails(): array {
        return [
            'resourceType' => $this->resourceTyp,
            'resourceId' => $this->resourceId
        ];
    }
}

class UnauthorizedException extends ApiException {
    protected $statusCode = 401;
}

class ForbiddenException extends ApiException {
    protected $statusCode = 403;
}

// API-Handler mit Fehlerbehandlung
class ApiHandler {
    public function handleRequest(string $method, string $pfad, array $daten = []): array {
        try {
            // HTTP-Methode prüfen
            if (!in_array($method, ['GET', 'POST', 'PUT', 'DELETE'])) {
                throw new ApiException("Methode $method nicht unterstützt", 405);
            }
            
            // Pfad analysieren und entsprechende Aktion ausführen
            $pfadTeile = explode('/', trim($pfad, '/'));
            $ressource = $pfadTeile[0] ?? '';
            $id = $pfadTeile[1] ?? null;
            
            switch ($ressource) {
                case 'benutzer':
                    return $this->handleBenutzerRequest($method, $id, $daten);
                case 'produkte':
                    return $this->handleProdukteRequest($method, $id, $daten);
                default:
                    throw new NotFoundException('Ressource', $ressource);
            }
        } catch (ApiException $e) {
            http_response_code($e->getStatusCode());
            return $e->getResponseData();
        } catch (Exception $e) {
            // Unerwartete Fehler protokollieren
            error_log("Unerwarteter Fehler: " . $e->getMessage() . "\n" . $e->getTraceAsString());
            
            http_response_code(500);
            return [
                'error' => [
                    'code' => 500,
                    'message' => 'Interner Serverfehler'
                ]
            ];
        }
    }
    
    private function handleBenutzerRequest(string $method, ?string $id, array $daten): array {
        switch ($method) {
            case 'GET':
                if ($id === null) {
                    return ['benutzer' => [
                        ['id' => 1, 'name' => 'Max Mustermann'],
                        ['id' => 2, 'name' => 'Erika Musterfrau']
                    ]];
                }
                
                // Simuliere Benutzersuche
                if ($id == '1') {
                    return ['benutzer' => ['id' => 1, 'name' => 'Max Mustermann']];
                } elseif ($id == '2') {
                    return ['benutzer' => ['id' => 2, 'name' => 'Erika Musterfrau']];
                } else {
                    throw new NotFoundException('Benutzer', $id);
                }
                
            case 'POST':
                // Validierung
                $fehler = [];
                if (empty($daten['name'])) {
                    $fehler['name'] = 'Name ist erforderlich';
                }
                if (empty($daten['email'])) {
                    $fehler['email'] = 'E-Mail ist erforderlich';
                } elseif (!filter_var($daten['email'], FILTER_VALIDATE_EMAIL)) {
                    $fehler['email'] = 'Ungültiges E-Mail-Format';
                }
                
                if (!empty($fehler)) {
                    throw new ValidationException($fehler);
                }
                
                // Simuliere Benutzerregistrierung
                return [
                    'benutzer' => [
                        'id' => 3,
                        'name' => $daten['name'],
                        'email' => $daten['email']
                    ],
                    'message' => 'Benutzer erfolgreich erstellt'
                ];
                
            // Weitere Methoden...
            
            default:
                throw new ApiException("Methode $method für Ressource 'benutzer' nicht unterstützt", 405);
        }
    }
    
    private function handleProdukteRequest(string $method, ?string $id, array $daten): array {
        // Implementierung ähnlich wie bei handleBenutzerRequest
        throw new ApiException("Produkt-API noch nicht implementiert", 501);
    }
}

// Verwendung des API-Handlers
$methode = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$pfad = $_SERVER['PATH_INFO'] ?? '/';
$daten = json_decode(file_get_contents('php://input'), true) ?? [];

$handler = new ApiHandler();
$ergebnis = $handler->handleRequest($methode, $pfad, $daten);

// JSON-Antwort senden
header('Content-Type: application/json');
echo json_encode($ergebnis);

Dieses Beispiel zeigt einen umfassenden Ansatz für die Fehlerbehandlung in einer Web-API, mit spezifischen Exception-Klassen für verschiedene Fehlertypen und einer einheitlichen Fehlerantwortstruktur.

18.14 Best Practices für die Fehlerbehandlung

  1. Exceptions für außergewöhnliche Situationen verwenden: Exceptions sollten für Fehler reserviert sein, nicht für normale Programmabläufe.

  2. Spezifische Exception-Klassen erstellen: Entwickeln Sie Klassen, die anwendungsspezifische Informationen enthalten und eine präzise Fehlerbehandlung ermöglichen.

  3. Exceptions auf angemessener Ebene abfangen: Fangen Sie Exceptions dort ab, wo sie sinnvoll behandelt werden können, nicht notwendigerweise dort, wo sie geworfen werden.

  4. Kontextinformationen hinzufügen: Stellen Sie sicher, dass Exceptions genügend Informationen enthalten, um den Fehler zu verstehen und zu beheben.

  5. Fehler protokollieren, aber sensible Daten schützen: Protokollieren Sie Fehlerdetails für Debugging-Zwecke, zeigen Sie aber keine sensiblen Informationen in Benutzerantworten an.

  6. Exception-Hierarchie planen: Entwerfen Sie eine sinnvolle Hierarchie von Exception-Klassen für Ihre Anwendung.

  7. Ressourcen immer bereinigen: Verwenden Sie finally-Blöcke, um sicherzustellen, dass Ressourcen ordnungsgemäß freigegeben werden.

  8. Unterschied zwischen Exceptions und Errors verstehen: Exceptions repräsentieren abfangbare Fehler, während Errors in der Regel schwerwiegendere Probleme darstellen, die nicht immer vollständig wiederhergestellt werden können.

  9. Globalen Exception-Handler für unerwartete Fehler: Implementieren Sie einen Fallback-Handler für nicht abgefangene Exceptions.

  10. Try-Catch-Blöcke nicht überschachteln: Vermeiden Sie zu viele verschachtelte Try-Catch-Blöcke, da dies die Codelesbarkeit beeinträchtigt.