Lekcja 12: Obsługa Błędów i Wyjątków w PHP

Witaj w dwunastej lekcji kursu PHP! Do tej pory skupialiśmy się na pisaniu kodu, który działa poprawnie w idealnych warunkach. Jednak w rzeczywistym świecie aplikacje często napotykają nieoczekiwane sytuacje: brakujące pliki, problemy z połączeniem do bazy danych, nieprawidłowe dane wejściowe od użytkownika, czy błędy logiczne w samym kodzie. Kluczowym aspektem tworzenia solidnych i niezawodnych aplikacji jest umiejętność eleganckiego radzenia sobie z takimi problemami. W tej lekcji omówimy mechanizmy obsługi błędów i wyjątków w PHP, które pozwalają na kontrolowane zarządzanie sytuacjami awaryjnymi.

Rodzaje Błędów w PHP

PHP rozróżnia kilka typów błędów, które mogą wystąpić podczas wykonywania skryptu. Zrozumienie ich natury jest pierwszym krokiem do efektywnej obsługi.

Konfiguracja Wyświetlania Błędów

Sposób, w jaki PHP raportuje błędy, można kontrolować za pomocą dyrektyw konfiguracyjnych w pliku php.ini lub dynamicznie w skrypcie za pomocą funkcji ini_set().

<?php
// Ustawienia zalecane podczas developmentu (na początku skryptu)
ini_set('display_errors', '1'); // Wyświetlaj błędy
ini_set('display_startup_errors', '1'); // Wyświetlaj błędy startowe (np. parse errors)
error_reporting(E_ALL); // Raportuj wszystkie typy błędów

echo $niezdefiniowanaZmienna; // Wygeneruje Notice

// include 'nieistniejacy_plik.php'; // Wygeneruje Warning i Fatal Error (jeśli display_errors jest on)

// W środowisku produkcyjnym:
// ini_set('display_errors', '0');
// error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT); // Lub bardziej restrykcyjnie
// ini_set('log_errors', '1');
// ini_set('error_log', '/sciezka/do/php-error.log');
?>

Obsługa Błędów za pomocą Niestandardowych Handlerów

PHP pozwala na zdefiniowanie własnej funkcji, która będzie wywoływana za każdym razem, gdy wystąpi błąd (z wyjątkiem niektórych błędów fatalnych, jak błędy parsowania czy braku pamięci). Robi się to za pomocą funkcji set_error_handler().

<?php
// Definicja niestandardowego handlera błędów
function mojErrorHandler($poziomBledu, $komunikatBledu, $plikBledu, $liniaBledu) {
    echo "<div style='border: 1px solid red; padding: 10px; margin: 10px;'>";
    echo "<strong>Wystąpił błąd!</strong><br>";
    echo "Poziom: $poziomBledu<br>";
    echo "Komunikat: " . htmlspecialchars($komunikatBledu) . "<br>";
    echo "Plik: " . htmlspecialchars($plikBledu) . "<br>";
    echo "Linia: $liniaBledu<br>";
    echo "</div>";

    // Zwrócenie true zapobiega wykonaniu domyślnego handlera błędów PHP
    // Zwrócenie false (lub brak return) spowoduje, że domyślny handler też się wykona
    return true;
}

// Ustawienie niestandardowego handlera
set_error_handler("mojErrorHandler");

// Wywołanie błędu, który zostanie przechwycony przez nasz handler
echo $testZmiennej; // Notice: Undefined variable

$wynikDzielenia = 10 / 0; // Warning: Division by zero

// Aby przywrócić domyślny handler błędów:
// restore_error_handler();
?>

Niestandardowe handlery błędów dają pełną kontrolę nad tym, jak błędy są prezentowane lub logowane. Można w nich np. wysyłać powiadomienia e-mail do administratora, zapisywać szczegółowe informacje do bazy danych, czy wyświetlać użytkownikowi przyjazną stronę błędu.

Wyjątki (Exceptions)

Wyjątki to obiektowy mechanizm obsługi błędów, wprowadzony w PHP 5. Pozwalają one na bardziej strukturalne i elastyczne zarządzanie sytuacjami wyjątkowymi w kodzie. Zamiast polegać na tradycyjnych błędach PHP, można "rzucać" (throw) obiekty wyjątków, które następnie mogą być "łapane" (catch) i obsługiwane w odpowiednich blokach kodu.

Podstawowa idea wyjątków opiera się na trzech słowach kluczowych: try, catch i throw.

PHP ma wbudowaną klasę bazową Exception, oraz wiele jej podklas (np. InvalidArgumentException, RuntimeException, PDOException). Można również tworzyć własne, niestandardowe klasy wyjątków, dziedzicząc po klasie Exception lub jej podklasach.

<?php
function podzielLiczby(float $dzielna, float $dzielnik): float {
    if ($dzielnik == 0) {
        // Rzucenie wyjątku, jeśli dzielnik to zero
        throw new Exception("Nie można dzielić przez zero!");
    }
    return $dzielna / $dzielnik;
}

try {
    $wynik1 = podzielLiczby(10, 2);
    echo "Wynik dzielenia 10 / 2 = $wynik1<br>"; // 5

    $wynik2 = podzielLiczby(8, 0);
    // Ten kod się nie wykona, bo powyżej zostanie rzucony wyjątek
    echo "Wynik dzielenia 8 / 0 = $wynik2<br>"; 

} catch (Exception $e) {
    // Przechwycenie i obsługa wyjątku
    echo "<strong>Wystąpił wyjątek:</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
    echo "Plik: " . htmlspecialchars($e->getFile()) . "<br>";
    echo "Linia: " . $e->getLine() . "<br>";
    // echo "Ślad stosu (trace): <pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre><br>";
}

echo "Kontynuacja wykonywania skryptu po bloku try-catch.<br>";
?>

Obiekt wyjątku ($e w przykładzie) dostarcza przydatnych metod do uzyskania informacji o błędzie:

Wiele Bloków catch

Można użyć wielu bloków catch po jednym bloku try, aby obsługiwać różne typy wyjątków w różny sposób. PHP spróbuje dopasować rzucony wyjątek do pierwszego pasującego bloku catch (od góry do dołu).

<?php
class MojWyjatekPliku extends Exception {}
class MojWyjatekBazyDanych extends Exception {}

function operacjaNaPliku(string $sciezka) {
    if (!file_exists($sciezka)) {
        throw new MojWyjatekPliku("Plik '$sciezka' nie istnieje.");
    }
    if (!is_readable($sciezka)) {
        throw new MojWyjatekPliku("Nie można odczytać pliku '$sciezka'.");
    }
    // ... logika operacji na pliku
    echo "Operacja na pliku '$sciezka' zakończona sukcesem.<br>";
}

function operacjaNaBazie(bool $polaczenieUdane) {
    if (!$polaczenieUdane) {
        throw new MojWyjatekBazyDanych("Błąd połączenia z bazą danych.", 500);
    }
    // ... logika operacji na bazie
    echo "Operacja na bazie danych zakończona sukcesem.<br>";
}

try {
    operacjaNaPliku("istniejacy_plik.txt"); // Załóżmy, że istnieje i jest czytelny
    // operacjaNaPliku("nie_istnieje.txt"); // Rzuci MojWyjatekPliku
    operacjaNaBazie(true);
    // operacjaNaBazie(false); // Rzuci MojWyjatekBazyDanych
    // throw new InvalidArgumentException("Nieprawidłowy argument!"); // Rzuci InvalidArgumentException

} catch (MojWyjatekPliku $e) {
    echo "<strong>Błąd pliku:</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
} catch (MojWyjatekBazyDanych $e) {
    echo "<strong>Błąd bazy danych (kod: " . $e->getCode() . "):</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
} catch (Exception $e) { // Ogólny handler dla innych wyjątków dziedziczących po Exception
    echo "<strong>Inny błąd:</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
}

echo "Dalsza część skryptu.<br>";

// Tworzenie pliku dla testu:
// file_put_contents("istniejacy_plik.txt", "test");
?>

Ważne jest, aby bardziej specyficzne typy wyjątków łapać przed bardziej ogólnymi (np. Exception powinien być na końcu listy bloków catch).

Blok finally (od PHP 5.5)

Blok finally można dodać po ostatnim bloku catch. Kod w bloku finally jest wykonywany zawsze po zakończeniu bloku try (i ewentualnie catch), niezależnie od tego, czy wyjątek został rzucony i złapany, czy nie. Jest to przydatne do wykonywania operacji czyszczących, takich jak zamykanie połączeń z bazą danych, zamykanie plików, zwalnianie zasobów itp.

<?php
function przetwarzajZasob($powinienRzucicWyjatek) {
    $zasob = null; // Symulacja otwarcia zasobu
    try {
        echo "Otwieranie zasobu...<br>";
        $zasob = "OTWARTY_ZASOB";

        if ($powinienRzucicWyjatek) {
            throw new Exception("Problem podczas przetwarzania zasobu!");
        }
        echo "Przetwarzanie zasobu zakończone sukcesem.<br>";

    } catch (Exception $e) {
        echo "<strong>Złapano wyjątek:</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
    } finally {
        // Ten blok wykona się zawsze
        echo "Zamykanie zasobu... (aktualny stan: $zasob)<br>";
        $zasob = "ZAMKNIETY_ZASOB"; // Symulacja zamknięcia
        echo "Zasób został zamknięty.<br>";
    }
}

echo "<h3>Test 1: Bez wyjątku</h3>";
przetwarzajZasob(false);

echo "<h3>Test 2: Z wyjątkiem</h3>";
przetwarzajZasob(true);
?>

Niestandardowy Handler Wyjątków

Podobnie jak dla błędów, można ustawić globalny, niestandardowy handler dla nieprzechwyconych wyjątków za pomocą funkcji set_exception_handler(). Ta funkcja zostanie wywołana, jeśli wyjątek zostanie rzucony i nie zostanie złapany przez żaden blok catch.

<?php
function mojExceptionHandler(Throwable $wyjatek) { // Od PHP 7, Throwable łapie Exception i Error
    echo "<div style='background-color: #ffdddd; border: 1px solid darkred; padding: 15px; margin: 10px;'>";
    echo "<h2>Nieprzechwycony Wyjątek!</h2>";
    echo "<p><strong>Komunikat:</strong> " . htmlspecialchars($wyjatek->getMessage()) . "</p>";
    echo "<p><strong>Plik:</strong> " . htmlspecialchars($wyjatek->getFile()) . " (linia: " . $wyjatek->getLine() . ")</p>";
    echo "<p><strong>Ślad stosu:</strong></p>";
    echo "<pre>" . htmlspecialchars($wyjatek->getTraceAsString()) . "</pre>";
    echo "</div>";
    // W środowisku produkcyjnym tutaj logowalibyśmy błąd i wyświetlali użytkownikowi ogólną stronę błędu.
}

set_exception_handler('mojExceptionHandler');

// throw new Exception("To jest testowy nieprzechwycony wyjątek!");

// Przykład, który może rzucić Error (np. TypeError jeśli strict_types=1)
// function testTypowania(int $a) { var_dump($a); }
// testTypowania("nie int");
?>

Od PHP 7, zarówno tradycyjne błędy PHP (takie jak Fatal Errors), jak i wyjątki implementują interfejs Throwable. Oznacza to, że można użyć catch (Throwable $t) do łapania zarówno wyjątków, jak i wielu błędów, które wcześniej nie były łapalne. Klasy Error i Exception dziedziczą po Throwable.

Kiedy używać Błędów, a kiedy Wyjątków?

Dobrą praktyką jest konwertowanie krytycznych błędów PHP na wyjątki za pomocą niestandardowego error handlera, aby móc je obsługiwać w spójny sposób za pomocą bloków try-catch.

Podsumowanie Lekcji

W tej lekcji nauczyliśmy się, jak PHP radzi sobie z błędami i sytuacjami wyjątkowymi. Poznaliśmy różne typy błędów, sposoby ich konfiguracji i raportowania, oraz jak tworzyć niestandardowe handlery błędów. Zgłębiliśmy również potężny mechanizm wyjątków, ucząc się, jak rzucać (throw), łapać (try-catch) i obsługiwać wyjątki, w tym użycie wielu bloków catch oraz bloku finally. Dowiedzieliśmy się także o interfejsie Throwable i możliwości ustawienia globalnego handlera dla nieprzechwyconych wyjątków.

Efektywna obsługa błędów i wyjątków jest fundamentem tworzenia stabilnych, niezawodnych i bezpiecznych aplikacji PHP. W następnej lekcji zajmiemy się praktycznymi aspektami pracy z systemem plików – odczytem, zapisem i zarządzaniem plikami oraz katalogami.


Zadanie praktyczne

Napisz skrypt PHP, który:

  1. Definiuje funkcję obliczPierwiastek($liczba), która przyjmuje liczbę. Jeśli liczba jest ujemna, funkcja powinna rzucić wyjątek InvalidArgumentException z odpowiednim komunikatem. W przeciwnym razie powinna zwrócić pierwiastek kwadratowy z liczby (użyj sqrt()).
  2. W bloku try-catch wywołaj tę funkcję dwukrotnie: raz z liczbą dodatnią, a raz z liczbą ujemną.
  3. W bloku catch przechwyć InvalidArgumentException i wyświetl komunikat błędu. Dodaj również blok finally, który zawsze wyświetli komunikat "Operacja obliczania pierwiastka zakończona.".
  4. Ustaw niestandardowy error handler (set_error_handler), który będzie przechwytywał błędy typu E_USER_NOTICE i wyświetlał je w specjalnie sformatowanym divie. Następnie wygeneruj taki błąd za pomocą trigger_error("To jest testowe powiadomienie użytkownika.", E_USER_NOTICE);.

Pokaż przykładowe rozwiązanie
<?php
// 4. Niestandardowy error handler dla E_USER_NOTICE
function mojNoticeHandler($poziom, $komunikat, $plik, $linia) {
    if ($poziom === E_USER_NOTICE) {
        echo "<div style='background-color: #e6f7ff; border: 1px solid #91d5ff; padding: 10px; margin: 10px;'>";
        echo "<strong>Powiadomienie Użytkownika:</strong> " . htmlspecialchars($komunikat);
        echo " (Plik: " . htmlspecialchars($plik) . ", Linia: $linia)";
        echo "</div>";
        return true; // Zapobiegamy domyślnej obsłudze
    }
    return false; // Inne błędy obsługuje domyślny handler
}
set_error_handler('mojNoticeHandler');

// 1. Funkcja obliczPierwiastek
function obliczPierwiastek(float $liczba): float {
    if ($liczba < 0) {
        throw new InvalidArgumentException("Nie można obliczyć pierwiastka kwadratowego z liczby ujemnej: $liczba");
    }
    return sqrt($liczba);
}

// 2. i 3. Blok try-catch-finally
echo "<h3>Testowanie obliczania pierwiastka:</h3>";

$liczbyDoTestu = [16, -4];

foreach ($liczbyDoTestu as $l) {
    try {
        echo "Próba obliczenia pierwiastka z: $l<br>";
        $wynik = obliczPierwiastek($l);
        echo "Pierwiastek z $l wynosi: $wynik<br>";
    } catch (InvalidArgumentException $e) {
        echo "<strong style='color: red;'>Złapano wyjątek InvalidArgumentException:</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
    } catch (Exception $e) { // Ogólny catch dla innych ewentualnych wyjątków
        echo "<strong style='color: orange;'>Złapano inny wyjątek:</strong> " . htmlspecialchars($e->getMessage()) . "<br>";
    } finally {
        echo "<em>Operacja obliczania pierwiastka dla liczby $l zakończona.</em><br><hr>";
    }
}

// 4. Wygenerowanie E_USER_NOTICE
echo "<h3>Testowanie niestandardowego handlera powiadomień:</h3>";
trigger_error("To jest testowe powiadomienie użytkownika.", E_USER_NOTICE);

restore_error_handler(); // Przywrócenie domyślnego handlera
?>
            

Zadanie do samodzielnego wykonania

Stwórz klasę KonfiguracjaAplikacji, która w konstruktorze próbuje wczytać dane konfiguracyjne z pliku JSON (np. config.json). Jeśli plik nie istnieje, konstruktor powinien rzucić niestandardowy wyjątek BrakPlikuKonfiguracyjnegoException (stwórz tę klasę, dziedzicząc po Exception). Jeśli plik istnieje, ale jego zawartość nie jest poprawnym JSON-em, konstruktor powinien rzucić wyjątek NiepoprawnyFormatKonfiguracjiException (również stwórz tę klasę). W głównym skrypcie, użyj bloku try-catch do utworzenia instancji tej klasy i obsłuż oba typy niestandardowych wyjątków, wyświetlając odpowiednie komunikaty. Przetestuj dla scenariuszy: brak pliku, plik z błędnym JSON-em, plik z poprawnym JSON-em.


FAQ - Obsługa Błędów i Wyjątków

Jaka jest różnica między Error a Exception w PHP 7+?

Obie klasy, Error i Exception, implementują interfejs Throwable. Error jest używane do reprezentowania wewnętrznych błędów PHP, które wcześniej były błędami fatalnymi (np. błędy typu, błędy parsowania). Exception jest klasą bazową dla wyjątków generowanych przez programistę lub biblioteki. Można łapać oba za pomocą catch (Throwable $t).

Czy blok finally wykona się, jeśli w bloku catch zostanie rzucony kolejny wyjątek?

Tak, blok finally jest zaprojektowany tak, aby wykonać się niezależnie od tego, co dzieje się w blokach try i catch, nawet jeśli w catch zostanie rzucony nowy wyjątek lub użyta zostanie instrukcja return, break czy continue.

Czy mogę mieć blok try bez bloku catch, ale z blokiem finally?

Tak, od PHP 5.5 można mieć konstrukcję try-finally bez żadnego bloku catch. Jest to przydatne, gdy chcemy zapewnić wykonanie kodu czyszczącego (w finally), ale nie chcemy bezpośrednio obsługiwać wyjątku w tym miejscu (może on być wtedy propagowany wyżej na stosie wywołań).

Jak najlepiej logować błędy i wyjątki w aplikacji produkcyjnej?

W środowisku produkcyjnym należy wyłączyć wyświetlanie błędów użytkownikowi (display_errors = off). Zamiast tego, wszystkie błędy i wyjątki powinny być logowane do pliku (log_errors = on, error_log) lub do dedykowanego systemu monitorowania błędów (np. Sentry, Bugsnag). Niestandardowe handlery błędów i wyjątków mogą być użyte do formatowania logów i wysyłania powiadomień do administratorów.

Czy użycie @ przed wyrażeniem do tłumienia błędów jest dobrą praktyką?

Operator kontroli błędów @ (np. @fopen(...)) tłumi komunikaty błędów generowane przez dane wyrażenie. Chociaż może być użyteczny w niektórych specyficznych sytuacjach (np. gdy chcemy sprawdzić, czy funkcja zwróci false w przypadku błędu i obsłużyć to ręcznie), generalnie jego nadużywanie jest uważane za złą praktykę. Utrudnia debugowanie i może maskować poważne problemy. Lepszym podejściem jest odpowiednia konfiguracja raportowania błędów i użycie mechanizmów wyjątków.

Co to jest "bąbelkowanie" (bubbling) wyjątków?

Jeśli wyjątek zostanie rzucony wewnątrz funkcji i nie zostanie tam złapany przez blok try-catch, jest on propagowany (bąbelkuje) w górę stosu wywołań do funkcji wywołującej. Jeśli tam również nie zostanie złapany, proces ten kontynuuje się aż do globalnego zakresu. Jeśli wyjątek dotrze do globalnego zakresu i nie zostanie złapany, a ustawiony jest globalny handler wyjątków (set_exception_handler), to on zostanie wywołany. W przeciwnym razie skrypt zakończy działanie z błędem fatalnym.

Czy mogę rzucać stringi lub liczby jako wyjątki?

Nie, w PHP można rzucać (throw) tylko obiekty, które są instancjami klasy Throwable lub jej podklas (czyli Error lub Exception oraz ich pochodne). Próba rzucenia czegoś innego spowoduje błąd fatalny.