Menu

Wyjątki w PHP: throw i własne klasy wyjątków

Wyjątek w PHP rzucasz przez throw new Exception('message');, a wywołujący odczytuje go przez $e->getMessage() w bloku catch. Poznaj wbudowane klasy wyjątków, pisanie własnych wyjątków, getCode(), getLine() i getPrevious(), throw jako wyrażenie oraz błędy rzucane przez samo PHP, takie jak ValueError i TypeError.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Wyjątek w PHP rzucasz przez throw new Exception('message');. Wykonanie zatrzymuje się w tej linii i przeskakuje do najbliższego bloku catch, który akceptuje typ wyjątku, gdzie $e->getMessage() zwraca komunikat. Używaj konkretnej klasy, takiej jak InvalidArgumentException, aby wywołujący mogli rozróżniać problemy.

Wyjątek jest obiektem. throw przekazuje go w górę stosu wywołań do pierwszego wywołującego z pasującym catch, a ten decyduje, co zrobić; funkcja, która znalazła problem, nie musi tego wiedzieć. Mechanikę przechwytywania, finally i łapanie kilku typów opisuje strona try catch.

Co zawiera obiekt wyjątku

Każdy wyjątek niesie komunikat, kod całkowity, plik i linię, w której go utworzono, oraz ślad stosu. Konstruktor przyjmuje ($message, $code, $previous), wszystkie opcjonalne:

getLine() to linia throw new, a nie linia catch. Ślad wymienia wywołania, które tam doprowadziły (#0 /home/index.php(8): findOrder()), i zwykle to pierwsza rzecz do przeczytania, gdy błąd pojawi się w logu. Rzutowanie wyjątku na string (echo $e; albo (string) $e) wypisuje to wszystko w standardowym formacie PHP.

Wbudowane klasy wyjątków

PHP ma rodzinę klas wyjątków w Standard PHP Library (SPL). Rzucanie tej, która nazywa problem, sprawia, że kod czyta się lepiej, a wywołujący mogą łapać wąsko. Wszystkie rozszerzają Exception:

KlasaRzuć ją, gdy
InvalidArgumentExceptionargument ma złą postać: pusta nazwa, nieznana opcja
DomainExceptionwartość jest poza zbiorem, który ma sens: ujemna cena
OutOfRangeExceptionkod poprosił o indeks, który nigdy nie może istnieć (błąd wywołującego)
OutOfBoundsExceptionbrakuje klucza lub indeksu w danych znanych dopiero w czasie działania
RangeExceptionobliczony wynik wypada poza poprawny zakres w czasie działania
LengthExceptioncoś jest za długie lub za krótkie
RuntimeExceptionproblem wykrywalny dopiero w czasie działania: pełny dysk, przekroczony czas
UnexpectedValueExceptionfunkcja zwróciła lub dostała wartość typu, którego się nie spodziewałeś
LogicExceptionsam kod jest zły, to błąd, a nie złe dane
JsonExceptionrzucany przez json_encode()/json_decode() z JSON_THROW_ON_ERROR

InvalidArgumentException, DomainException, LengthException i OutOfRangeException rozszerzają LogicException; OutOfBoundsException, RangeException i UnexpectedValueException rozszerzają RuntimeException. Rodziców dowolnej klasy możesz sprawdzić sam:

TypeError ma jako rodzica Error, a nie Exception: należy do drugiej rodziny, opisanej niżej.

Błędy rzucane przez samo PHP

Od PHP 7, a jeszcze bardziej od PHP 8, własne funkcje i operatory PHP rzucają podklasy Error dla problemów, które kiedyś były ostrzeżeniami. Łapiesz je tak samo, ale catch (Exception $e) do nich nie pasuje:

DivisionByZeroError, TypeError, ValueError, ArgumentCountError, UnhandledMatchError i zwykły Error rozszerzają Error. Każdy z nich częściej oznacza błąd w kodzie wywołującym niż złe dane, dlatego leżą poza drzewem Exception. To declare(strict_types=1) sprawia, że str_repeat(5, 2) jest TypeError; bez tego PHP zamieniłoby 5 na "5".

Własna klasa wyjątku

Często wystarczy jednolinijkowa podklasa: sama nazwa klasy niesie znaczenie, a wywołujący mogą łapać dokładnie ten typ. Gdy łapiący potrzebuje danych (jakie zamówienie, jaka kwota), dodaj właściwości i przekaż komunikat wyżej przez parent::__construct():

Rodzica wybieraj starannie: rozszerzenie RuntimeException (zamiast zwykłego Exception) oznacza, że kod łapiący RuntimeException obsłuży też twój wyjątek. Mała hierarchia, jeden bazowy wyjątek na bibliotekę lub moduł z konkretnymi podklasami pod nim, pozwala wywołującym wybierać między „łap wszystko z płatności” a „łap tylko ten jeden przypadek”.

Łańcuch wyjątków przez previous

Gdy łapiesz niskopoziomowy wyjątek i rzucasz wysokopoziomowy, przekaż oryginał jako trzeci argument. Nic nie ginie: getPrevious() cofa się do pierwotnej przyczyny.

Użytkownik widzi „customers.csv could not be imported”; log, przechodząc łańcuch, dostaje też „row 2 has 2 columns”. Jeśli wyjątek z łańcuchem nigdy nie zostanie złapany, krytyczny błąd PHP wypisuje cały łańcuch: najpierw pierwotną przyczynę, potem każde opakowanie pod linią Next.

throw jako wyrażenie

Od PHP 8.0 throw jest wyrażeniem, więc może stać wszędzie tam, gdzie oczekiwana jest wartość: po ??, w operatorze trójargumentowym, w funkcji strzałkowej. Dzięki temu „pobierz to albo zgłoś błąd” mieści się w jednej linii:

Kiedy nie rzucać

Wyjątki są na sytuacje, których bieżąca funkcja nie potrafi obsłużyć i których normalny przebieg się nie spodziewa. Wyszukiwanie bez wyników, puste pole opcjonalne czy niezalogowany użytkownik to zwykłe wyniki: zwróć null, false albo pustą tablicę i pozwól wywołującemu to sprawdzić, a wyjątki zostaw na przypadki, w których kontynuowanie byłoby błędem.

Dwa kolejne nawyki utrzymują czytelność kodu z wyjątkami: nigdy nie łap wyjątku tylko po to, by go zignorować (puste catch (Exception $e) {} ukrywa następny prawdziwy błąd), i rzucaj konkretne klasy zamiast samego Exception, żeby wywołujący nigdy nie musiał łapać wszystkiego, by obsłużyć jeden przypadek.

Najczęściej zadawane pytania

Jak rzucić wyjątek w PHP?

Utwórz obiekt wyjątku i go rzuć: throw new InvalidArgumentException('Quantity must be positive');. Wykonanie zatrzymuje się w tej linii i przeskakuje do najbliższego pasującego bloku catch; jeśli go nie ma, skrypt kończy się krytycznym błędem „Uncaught”.

Jak utworzyć własny wyjątek w PHP?

Rozszerz Exception albo jedną z jej podklas: class PaymentFailedException extends RuntimeException {}. Ta jedna linia wystarczy, by go rzucać i łapać po jego własnym typie. Dodaj właściwości i konstruktor, gdy łapiący potrzebuje dodatkowych danych, i wywołaj parent::__construct($message, $code, $previous).

Czym różni się getMessage od getCode?

getMessage() zwraca tekst przekazany jako pierwszy argument konstruktora, przeznaczony dla ludzi i logów. getCode() zwraca liczbę całkowitą przekazaną jako drugi argument (domyślnie 0), przydatną do sprawdzeń maszynowych, takich jak mapowanie błędów na kody statusu HTTP.

Jaki wyjątek rzucać w PHP?

Użyj wbudowanej klasy SPL pasującej do problemu: InvalidArgumentException dla złego argumentu, DomainException dla wartości spoza dozwolonego zakresu, RuntimeException dla porażek widocznych dopiero w czasie działania (pełny dysk, przekroczony czas), LogicException dla błędów programistycznych. Dla błędów, które wywołujący muszą rozróżniać, utwórz własną podklasę.

Czym jest łańcuch wyjątków w PHP?

Przekazaniem pierwotnego wyjątku jako trzeciego argumentu konstruktora przy rzucaniu nowego: throw new ImportException('Import failed', 0, $e);. Nowy wyjątek daje kontekst, a $e->getPrevious() na nim zwraca pierwotną przyczynę, więc żaden szczegół nie ginie.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ