7. Wyjątki i błędy
Gdy metoda klasy napotka błąd nieodwracalny (nieistniejący plik, brak połączenia z bazą danych, brak połączenia sieciowego), nie wyświetla ona błędu na konsoli (plik, baza danych), lecz generuje wyjątek. Wszystkie wyjątki dziedziczą po klasie [\Exception]. Oprócz wyjątków wewnętrzne działanie klasy PHP generuje również błędy, których klasą bazową jest klasa [\Error]. Obie klasy implementują interfejs PHP [\Throwable].
7.1. Struktura drzewa skryptów

7.2. Interfejs [\Throwable]
Interfejs [\Throwable] ma następujący wygląd:

Rola metod interfejsu jest następująca:

7.3. Wyjątki zdefiniowane w PHP 7
PHP 7 definiują kilka klas wyjątków:

- w [1] wyjątki zdefiniowane w PHP;
- W bibliotece [2] wyjątki z biblioteki SPL (Standardowa biblioteka PHP) z PHP 7. Biblioteka SPL stanowi zbiór klas i interfejsów przeznaczonych do rozwiązywania problemów, z którymi często borykają się programiści.
7.4. Błędy zdefiniowane w bibliotece PHP 7
PHP 7 definiuje kilka klas błędów:

Klasa [\Error] jest klasą nadrzędną wszystkich błędów zdefiniowanych w PHP. Klasa [ErrorException] umożliwia umieszczenie instancji klasy [\Error] w instancji klasy [\Exception]. Pozwala to na ujednolicenie obsługi błędów poprzez przetwarzanie wyłącznie wyjątków.
7.5. Przykład 1
Pierwszy przykład [exceptions-01.php] przedstawia zarówno błędy PHP, jak i wyjątek:
<?php
// wyświetlenie wszystkich błędów
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// kod --------
$var=[];
// nieznany klucz
print $var["abcd"];
// dzielenie przez zero
$var=7/0;
var_dump($var);
// tablica o stałych granicach
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// indeks poza granicami
$array[5]=8;
Komentarze
- wiersz 4: funkcja PHP otrzymuje polecenie zgłaszania wszystkich błędów. Drugim parametrem jest żądany poziom błędów:


- wiersz 5: żądamy wyświetlenia błędów na konsoli;
- wiersz 9: następuje próba dostępu do nieistniejącego elementu tablicy [$var];
- wiersz 11: wykonano dzielenie przez zero;
- wiersz 14: tworzona jest instancja klasy [SplFixedArray]. Klasa ta umożliwia utworzenie tablicy o stałych granicach i indeksach całkowitoliczbowych;
- wiersz 18: następuje dostęp do nieistniejącego elementu tablicy;
Wyniki
Komentarze
- wiersz 1 wyników: próba uzyskania dostępu do nieistniejącego klucza tablicy powoduje błąd PHP o poziomie [E_NOTICE]. Nie powoduje to przerwania wykonywania skryptu;
- wiersz 3 wyników: dzielenie liczby przez zero powoduje błąd PHP o poziomie [E_WARNING]. Nie przerywa to wykonywania skryptu;
- wiersze 6–9 wyników: próba dostępu do nieistniejącego indeksu tablicy o numerze [SplFixedArray] powoduje wyjątek typu [RuntimeException] i przerywa wykonywanie skryptu;
7.6. Obsługa wyjątków
Skrypt [exceptions-02.php] pokazuje, jak obsługiwać wyjątki:
<?php
// wyświetla się wszystkie błędy
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// kod jest otoczony blokiem try / catch
try {
$var = [];
// nieznany klucz
print $var["abcd"];
// dzielenie przez zero
$var = 7 / 0;
var_dump($var);
// tablica o stałych granicach
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// indeks poza zakresem
$array[5] = 8;
// sprawdzanie
print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
// \Throwable to interfejs implementowany przez większość błędów i wyjątków
// wyświetla się wyjątek
print "erreur, message : " . $ex->getMessage() . ", type : " . get_class($ex) . "\n";
}
Komentarze
- skrypt jest tym samym, który został przedstawiony w poprzednim akapicie. Tylko że teraz kod w wierszach 8–19, który może powodować błędy, został otoczony strukturą try / catch: jeśli kod w wierszach 8–21 wywoła wyjątek lub błąd, zostanie on obsłużony przez klauzulę catch w wierszach 22–26;
- wiersz 22: parametrem klauzuli [catch] jest typ wyjątku lub błędu, który chcemy obsłużyć. Ustawiając jako typ [\Throwable], który jest interfejsem, wskazujemy, że chcemy obsłużyć każdą instancję klasy implementującej interfejs [\Throwable]. Ponieważ wszystkie klasy błędów i wyjątków implementują ten interfejs, klauzula [catch] obsługuje tutaj każdy błąd/wyjątek zawarty w klasie;
- wiersz 19: instrukcja, która wywołuje błąd i powoduje wystąpienie wyjątku. Gdy tylko wystąpi wyjątek, następuje przejście do klauzuli [catch]. Kod znajdujący się za wierszem 19 nie zostanie zatem wykonany;
Wyniki
Komentarz do wyników
- wiersze 1 i 3: występują błędy o poziomie [E_NOTICE] i [E_WARNING]. Błędy te nie są wyjątkami, a zatem nie są obsługiwane przez klauzulę [catch];
- wiersz 5: pojawia się komunikat o błędzie zapisany w klauzuli [catch]. Oznacza to, że wystąpił wyjątek wywodzący się z [\Exception] lub błąd wywodzący się z [\Error]. Widzimy tutaj, że chodzi o klasę [\RuntimeException];
7.7. Parametry klauzuli [catch]
Przyjrzyjmy się poniższemu skryptowi [exceptions-03.php]:
<?php
// wyświetlane są wszystkie błędy
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// tablica o stałych granicach
$array = new \SplFixedArray(5);
try {
// indeks poza granicami
$array[5] = 8;
} catch (\Throwable $ex) {
// wyświetlanie komunikatu o błędzie
print "Erreur 1 : " . $ex->getMessage() . "\n";
}
try {
// wskaźnik poza granicami
$array[5] = 8;
} catch (\Exception $ex) {
// wyświetlanie komunikatu o błędzie
print "Erreur 2 : " . $ex->getMessage() . "\n";
}
try {
// wartość poza zakresem
$array[5] = 8;
} catch (\RuntimeException $ex) {
// wyświetlenie komunikatu o błędzie
print "Erreur 3 : " . $ex->getMessage() . "\n";
}
try {
// dzielenie przez 0
intdiv(5, 0);
} catch (\Throwable $ex) {
// wyświetlenie komunikatu o błędzie
print "Erreur 4 : " . $ex->getMessage() . "\n";
}
try {
// dzielenie przez 0
intdiv(5, 0);
} catch (\DivisionByzeroError $ex) {
// wyświetlenie komunikatu o błędzie
print "Erreur 5 : " . $ex->getMessage() . "\n";
}
try {
// dzielenie przez 0
intdiv(5, 0);
} catch (\Error $ex) {
// wyświetlenie komunikatu o błędzie
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
try {
// dzielenie przez 0
intdiv(5, 0);
} catch (\Exception $ex) {
// wyświetlenie komunikatu o błędzie
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
Komentarze
- wiersze 8–31: trzy różne sposoby obsługi wyjątku generowanego przez użycie nieprawidłowego indeksu w klasie [\SplFixedArray]. Widzieliśmy, że błąd ten generuje wyjątek typu [RuntimeException];
- wiersz 12: obsługuje błąd typu [\Throwable]. Jest to poprawne, ponieważ typ [RuntimeException] wywodzi się z typu [\Exception], który implementuje interfejs [\Throwable];
- wiersz 20: obsługuje błąd typu [\Exception]. Jest to poprawne, ponieważ typ [RuntimeException] wywodzi się z typu [\Exception];
- wiersz 28: obsługuje błąd typu [\RuntimeException]. Jest to preferowana metoda, ponieważ jest to dokładny typ wygenerowanego wyjątku;
- wiersze 32–62: 4 różne sposoby obsługi wyjątku generowanego przez funkcję [intdiv] w przypadku przekazania jej dzielnika równego 0. Funkcja [ intdiv ( int $dividend , int $divisor ) : int] wykonuje dzielenie całkowitoliczbowe $dividend / $divisor. Gdy dzielnik wynosi zero, generowany jest wyjątek [\DivisionByzeroError];
- wiersz 35: przechwytuje się każdy błąd implementujący interfejs [\Throwable]. Jest to poprawne;
- wiersz 43: przechwytuje się dokładny typ błędu: jest to preferowana metoda;
- wiersz 51: przechwytuje się typ [\Error]. Jest to poprawne, ponieważ klasa [DivisionByzeroError] dziedziczy po klasie [Error];
- wiersz 59: przechwytuje się typ [\Exception]. Jest to nieprawidłowe, ponieważ klasa [DivisionByzeroError] nie ma żadnego związku z klasą [\Exception];
Wyniki
7.8. Klauzula [finally]
Konstrukcja try / catch może zawierać trzeci element i przybrać postać try / catch / finally. Kod klauzuli [finally] jest wykonywany w obu poniższych przypadkach:
- klauzula [try] nie generuje wyjątku. Jest wówczas wykonywana w całości, a następnie wykonanie kodu przechodzi do klauzuli [finally], która jest wykonywana w całości;
- klauzula [try] generuje wyjątek. Jest ona wówczas wykonywana do instrukcji, która generuje wyjątek. Następnie wykonanie kodu przechodzi do klauzuli [catch], która jest wykonywana w całości. Następnie wykonanie kodu przechodzi do klauzuli [finally], która jest wykonywana w całości;
Ostatecznie kod klauzuli [finally] jest nadal wykonywany. Ten scenariusz jest przydatny w następującym przypadku:
- w klauzuli [try] kod uzyskał zasoby (pliki, bazy danych, połączenia sieciowe, kolejki). Zazwyczaj zasoby te są kosztowne pod względem pamięci. Należy je zatem zwolnić (najczęściej mówi się o „zamknięciu”) tak szybko, jak to możliwe;
- jeśli zasoby zostały pobrane w scenariuszu [try], ich zwrot zostanie umieszczony w scenariuszu [finally]. Dzięki temu mamy pewność, że w każdym przypadku (niezależnie od tego, czy wystąpił błąd, czy nie) pobrane zasoby zostaną zwrócone do systemu;
Poniższy skrypt [exemples/exceptions/exceptions-04.php] ilustruje działanie klauzuli [finally] w różnych sytuacjach:
<?php
// lub tworzy instancję wyjątku
$e = new \Exception("Erreur…");
var_dump($e);
// pierwszy test
try {
print "Premier test\n";
throw $e;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// drugi test
try {
print "Second test\n";
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// trzeci test
try {
print "Troisième test\n";
return;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
Komentarze do kodu
- wiersz 4: $e jest instancją predefiniowanej klasy [\Exception]. Zostanie ona wywołana w różnych miejscach;
- wiersze 8–15: wyjątek $e jest wywoływany w [try] (wiersz 10);
- wiersz 11: wyjątek [\Exception] zostaje przechwycony, a komunikat o błędzie zapisany na konsoli;
- wiersze 13–15: klauzula [finally] zapisuje komunikat. Zgodnie z tym, co zostało powiedziane wcześniej, komunikat ten powinien być zapisywany zawsze, niezależnie od tego, czy w [try] wystąpił błąd, czy nie;
- wiersze 18–24: w [try] nie ma błędu. W tym miejscu również powinno nastąpić przejście do [finally];
- wiersze 27–34: w bloku try znajduje się instrukcja [return] i nie ma błędu. Można zatem zastanawiać się, czy nastąpi przejście do klauzuli [finally]. Wykonanie pokazuje, że tak;
Wyniki
Przyjrzyjmy się innemu przypadkowi [exceptions-05.php]:
<?php
// czwarty test
try {
print "Quatrième test\n";
exit;
} finally {
print "Terminé\n";
}
Komentarze
- wiersz 6: instrukcja [exit] natychmiast zatrzymuje wykonywanie skryptu: klauzula [finally] nie jest wykonywana;
- wiersze 4–9: przykład konstrukcji try / catch / finally bez klauzuli [catch]. Jest to możliwe;
Wyniki
7.9. Tworzenie własnych klas wyjątków
W nieco większym projekcie warto rozróżniać różne błędy, umieszczając je w różnych klasach wyjątków. W poprzednim skrypcie widzieliśmy, że każdy wyjątek może zostać przechwycony przez klauzulę [catch (\Throwable]. Jest to zalecane, jeśli nie mamy pojęcia, jaki błąd został przechwycony, a sposób obsługi jest taki sam dla wszystkich błędów. Czasami tak właśnie jest, ale często trzeba dostosować sposób obsługi do konkretnego rodzaju błędu. W takim przypadku należy rozróżnić poszczególne błędy.
Przyjrzyjmy się poniższemu skryptowi [exceptions-06.php]:
<?php
// definiujemy własną rodzinę wyjątków
class Exception1 extends \RuntimeException {
}
class Exception2 extends \RuntimeException {
}
// lub korzystamy z naszych wyjątków
$e1 = new Exception1("Erreur1…");
var_dump($e1);
$e2 = new Exception2("Erreur2…");
var_dump($e2);
// pierwszy test
print ("premier test\n");
try {
// rzucamy wyjątek typu Exception1
throw $e1;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// drugi test
print ("second test\n");
try {
// wywołujemy wyjątek typu Exception2
throw $e2;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// trzeci test
print ("troisième test\n");
try {
// wywołano wyjątek typu Exception1
throw $e1;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
// czwarty test
print ("quatrième test\n");
try {
// wywołano wyjątek typu Exception2
throw $e2;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
Komentarze
- wiersze 4–10: definiujemy dwie klasy [Exception1] i [Exception2], obie wywodzące się z predefiniowanej klasy [\RuntimeException]. Treść tych klas jest pusta. Innymi słowy, wykorzystuje się je wyłącznie ze względu na ich typy: właśnie dzięki temu, że mają one różne typy, będziemy mogli rozróżnić te dwa wyjątki w klauzulach [catch];
- wiersze 13–16: definiujemy dwie zmienne $e1 i $e2 o typach odpowiednio [Exception1] i [Exception2];
- wiersze 20–29: zastosowano strukturę try / catch / catch. Pozwala to na obsługę różnych typów wyjątków za pomocą różnych klauzul [catch];
- wiersz 23: przechwytuje wyjątki typu [Exception1];
- wiersz 26: przechwytuje wyjątki typu [Exception2];
- wiersz 49: przechwytuje wyjątki typu [Exception1] lub (|) [Exception2];
Wyniki
Komentarze do wyników
- wiersze 1–17: „treść” wyjątku:
- wiersze 2–3: komunikat o błędzie;
- wiersze 6–7: kod błędu;
- wiersze 8–9: nazwa pliku, w którym wystąpił wyjątek;
- wiersze 10–11: wiersz, w którym wystąpił wyjątek;
- wiersze 15–16: poprzedni wyjątek. Wyjątek może zawierać w sobie inny wyjątek, tworząc w ten sposób stos wyjątków. Atrybut [previous] umożliwi wykorzystanie tego stosu;
7.10. Ponowne wywołanie wyjątku
Wyjątek może zostać wywołany wielokrotnie, jak pokazuje poniższy skrypt [exceptions-07.php]:
<?php
try {
try {
// wywołujemy wyjątek
throw new \Exception("test");
} catch (\Exception $ex) {
// ponownie rzucamy przechwycony wyjątek
throw $ex;
} finally {
// program poprawnie przechodzi do bloku finally
print "finally 1\n";
}
} catch (\Exception $ex2) {
// początkowy wyjątek jest prawidłowo przechwycony
print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
// program przejdzie do bloku finally
print "finally 2\n";
}
Komentarze
- wiersz 6: wywołujemy wyjątek;
- wiersz 7: przechwytuje się go;
- wiersz 9: ponownie wywołuje się wyjątek. Przechodzi on wówczas do struktury try / catch / finally wyższego poziomu;
- wiersz 14: ponownie przechwytuje się wyjątek;
- wiersze 10–12: wykonanie pokazuje, że nawet po wywołaniu [throw] w wierszu 9 program przechodzi do klauzuli [finally] w bloku try / catch / finally;
Wyniki
7.11. Wykorzystanie stosu wyjątków
Wyjątek może zawierać w sobie inny wyjątek, który z kolei może zawierać kolejny, tworząc w ten sposób stos wyjątków. Oto przykład [exceptions-08.php]:
Komentarze
- wiersze 4–14: definiują trzy klasy wyjątków wywodzące się z predefiniowanego wyjątku [RuntimeException];
- wiersz 17: instancja klasy [Exception3] jest zawarta w instancji klasy [Exception2], która z kolei jest zawarta w instancji klasy [Exception1]. W tym przypadku użyto konstruktora klasy [Exception]:
![]()
Trzeci parametr konstruktora pozwala na enkapsulację wyjątku. Może to być przydatne w następującym scenariuszu:
- definiujemy metodę M, która może wygenerować wyjątek typu [Exception1] i wyłącznie tego typu, na przykład ze względu na kompatybilność z interfejsem;
- jednak w metodzie M mogą wystąpić inne typy wyjątków. Aby zgłosić błąd do kodu wywołującego metodę M, należy zatem enkapsulować te wyjątki w typie [Exception1], który następnie zostanie wywołany. Pozwala to uniknąć utraty informacji zawartej w enkapsulowanym wyjątku, który był pierwotną przyczyną błędu;
- wiersze 20–27 pokazują, jak zarządzać stosem wyjątków wewnętrznych w ramach jednego wyjątku;
Wyniki
object(Exception1)#1 (7) {
["message":protected]=>
string(11) "Erreur 1…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(1)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception2)#2 (7) {
["message":protected]=>
string(11) "Erreur 2…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(2)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception3)#3 (7) {
["message":protected]=>
string(11) "Erreur 3…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(0)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
NULL
}
}
}
Erreur 1…
Erreur 2…
Erreur 3…