Wyjątki
🎯 Po tej lekcji obsłużysz wyjątek przez try/catch, użyjesz finally i stworzysz własną klasę wyjątku rozszerzającą Exception.
Do tej pory w kursie sygnalizowaliśmy błędy przez return false albo zwracanie tablicy z błędami. Działa, ale ma ograniczenia: kod wywołujący musi pamiętać o sprawdzeniu wartości zwracanej, a błąd może zostać po cichu zignorowany. PHP oferuje obiektowy mechanizm obsługi błędów: wyjątki (ang. exceptions).
Czym jest wyjątek?
Wyjątek to obiekt sygnalizujący, że coś poszło nie tak. Rzucasz go przez throw, a łapiesz przez try/catch. Gdy wyjątek zostanie rzucony i nikt go nie złapie — skrypt się zatrzymuje (Fatal Error).
throw i try/catch — podstawy
<?php
declare(strict_types=1);
function podziel(float $a, float $b): float
{
if ($b === 0.0) {
throw new \InvalidArgumentException('Dzielnik nie może być zerem.');
}
return $a / $b;
}
try {
echo podziel(10.0, 2.0); // wynik: 5
echo podziel(10.0, 0.0); // rzuca wyjątek
echo 'Tu już nie dojdziemy';
} catch (\InvalidArgumentException $e) {
echo 'Błąd: ' . $e->getMessage(); // wynik: Błąd: Dzielnik nie może być zerem.
}
Działanie:
thrownatychmiast przerywa bieżącą funkcję i szuka odpowiedniegocatch.- PHP przeszukuje bloki
catchod góry — wykonuje pierwszy pasujący. - Po obsłużeniu
catchkod kontynuuje za blokiemtry/catch.
Metody obiektu wyjątku
Klasa \Exception (i jej podklasy) dostarcza wiele przydatnych metod:
<?php
declare(strict_types=1);
try {
throw new \RuntimeException('Coś się posypało.', 500);
} catch (\RuntimeException $e) {
echo $e->getMessage(); // wynik: Coś się posypało.
echo $e->getCode(); // wynik: 500
echo $e->getFile(); // wynik: ścieżka do bieżącego pliku
echo $e->getLine(); // wynik: numer linii throw
}
finally — kod wykonywany zawsze
Blok finally wykona się niezależnie od tego, czy wyjątek wystąpił czy nie. Przydaje się do sprzątania zasobów (zamknięcia pliku, połączenia z bazą):
<?php
declare(strict_types=1);
function wczytajPlik(string $sciezka): string
{
$uchwyt = fopen($sciezka, 'r');
if ($uchwyt === false) {
throw new \RuntimeException("Nie można otworzyć pliku: $sciezka");
}
try {
$zawartosc = fread($uchwyt, filesize($sciezka));
if ($zawartosc === false) {
throw new \RuntimeException("Błąd odczytu pliku: $sciezka");
}
return $zawartosc;
} finally {
fclose($uchwyt); // wykona się zawsze — plik zostanie zamknięty
}
}
try {
$tekst = wczytajPlik('dane.txt');
echo $tekst;
} catch (\RuntimeException $e) {
echo 'Błąd: ' . $e->getMessage();
}
try lub catch zawiera return, finally i tak się wykona — przed faktycznym zwróceniem wartości. To gwarancja sprzątania.Łapanie wielu typów wyjątków
Możesz mieć wiele bloków catch — każdy obsługuje inny typ. PHP wykona pierwszy pasujący:
<?php
declare(strict_types=1);
function pobierzDane(int $id): string
{
if ($id <= 0) {
throw new \InvalidArgumentException("ID musi być dodatnie, podano: $id");
}
if ($id > 1000) {
throw new \RuntimeException("Nie znaleziono rekordu o ID: $id");
}
return "Dane dla ID $id";
}
try {
echo pobierzDane(-1);
} catch (\InvalidArgumentException $e) {
echo 'Błędny argument: ' . $e->getMessage();
// wynik: Błędny argument: ID musi być dodatnie, podano: -1
} catch (\RuntimeException $e) {
echo 'Błąd runtime: ' . $e->getMessage();
} catch (\Exception $e) {
echo 'Nieznany błąd: ' . $e->getMessage();
}
Możesz też złapać kilka typów w jednym catch przez |:
} catch (\InvalidArgumentException | \LengthException $e) {
echo 'Błąd logiki: ' . $e->getMessage();
}
\InvalidArgumentException) PRZED ogólnymi (np. \Exception). PHP wykonuje pierwszy pasujący — ogólny catch na początku "pochłonie" wszystko, zanim dotrzesz do szczegółowych.Hierarchia wbudowanych wyjątków
PHP dostarcza bogatą hierarchię wyjątków:
\Throwable
├── \Error (błędy wewnętrzne PHP: TypeError, ParseError, ...)
└── \Exception
├── \LogicException (błędy logiczne — można było zapobiec)
│ ├── \BadFunctionCallException
│ │ └── \BadMethodCallException
│ ├── \DomainException
│ ├── \InvalidArgumentException ← zły argument wejściowy
│ ├── \LengthException
│ └── \OutOfRangeException
└── \RuntimeException (błędy w czasie działania programu)
├── \OutOfBoundsException
├── \OverflowException
├── \RangeException
├── \UnderflowException
└── \UnexpectedValueException
Wybieraj typ wyjątku odpowiedni do sytuacji: \InvalidArgumentException gdy argument jest nieprawidłowy, \RuntimeException gdy coś poszło nie tak w czasie działania.
Własne klasy wyjątków
Tworzysz własny wyjątek przez rozszerzenie \Exception lub innej klasy z hierarchii:
<?php
declare(strict_types=1);
// Bazowy wyjątek dla domeny
class BledSklepuException extends \RuntimeException {}
// Specyficzne błędy
class ProduktNiedostepnyException extends BledSklepuException
{
public function __construct(string $nazwaProductu)
{
parent::__construct("Produkt '$nazwaProductu' jest niedostępny.");
}
}
class NiewystarczajaceSaldoException extends BledSklepuException
{
public function __construct(float $wymagane, float $dostepne)
{
parent::__construct(
sprintf('Wymagane: %.2f zł, dostępne: %.2f zł.', $wymagane, $dostepne)
);
}
}
// Użycie
function kupProdukt(string $nazwa, float $cena, float $saldo): void
{
if ($nazwa === 'Arbuz') {
throw new ProduktNiedostepnyException($nazwa);
}
if ($saldo < $cena) {
throw new NiewystarczajaceSaldoException($cena, $saldo);
}
echo "Kupiono: $nazwa";
}
try {
kupProdukt('Arbuz', 12.99, 50.0);
} catch (ProduktNiedostepnyException $e) {
echo 'Brak towaru: ' . $e->getMessage();
// wynik: Brak towaru: Produkt 'Arbuz' jest niedostępny.
} catch (NiewystarczajaceSaldoException $e) {
echo 'Brak środków: ' . $e->getMessage();
} catch (BledSklepuException $e) {
echo 'Ogólny błąd sklepu: ' . $e->getMessage();
}
Własne wyjątki dają dwie korzyści:
- Czytelność — nazwa wyjątku mówi dokładnie co poszło nie tak.
- Selektywne łapanie — możesz obsługiwać
ProduktNiedostepnyExceptioninaczej niżNiewystarczajaceSaldoException, mimo że oba dziedziczą poBledSklepuException.
Rzucanie wyjątku z innym jako przyczyną
Wyjątek może przyjąć trzeci parametr — „poprzednią przyczynę" (ang. previous). Przydatne gdy konwertujesz niskopoziomowy wyjątek na domenowy:
<?php
declare(strict_types=1);
class BazaDanychException extends \RuntimeException {}
function pobierzUzytkownika(int $id): array
{
try {
// symulacja błędu PDO
throw new \PDOException("Connection refused");
} catch (\PDOException $e) {
throw new BazaDanychException(
"Nie można pobrać użytkownika #$id",
0,
$e // poprzednia przyczyna — zachowuje ślad
);
}
}
try {
pobierzUzytkownika(1);
} catch (BazaDanychException $e) {
echo $e->getMessage(); // wynik: Nie można pobrać użytkownika #1
echo $e->getPrevious()?->getMessage(); // wynik: Connection refused
}
Brawo — dotarłeś do końca modułu OOP! Znasz teraz klasy, obiekty, konstruktory, enkapsulację, dziedziczenie, klasy abstrakcyjne, interfejsy, traity, enumy, przestrzenie nazw i wyjątki. W następnym module poznasz Composera — menedżera pakietów PHP, który łączy wszystkie te koncepcje w profesjonalny przepływ pracy.
Sprawdź się
Czy blok finally wykona się, jeśli w bloku try zostanie rzucony wyjątek, który zostanie złapany przez catch?
finally zawsze się wykona — zarówno gdy try zakończy się normalnie, jak i gdy wyjątek został złapany przez catch, jak i gdy wyjątek NIE został złapany (przed jego propagacją). Typowe zastosowanie: zamknięcie pliku lub połączenia z bazą danych.
Co się stanie z niezłapanym wyjątkiem (brak pasującego catch)?
Niezłapany wyjątek powoduje Fatal Error i natychmiast przerywa działanie skryptu. Dlatego ważne jest łapanie wyjątków w odpowiednich miejscach lub ustawianie globalnej obsługi przez set_exception_handler().
Jak stworzyć własną klasę wyjątku NieprawidlowyWiekException?
Własna klasa wyjątku to zwykła klasa rozszerzająca \Exception (lub inną klasę z hierarchii wyjątków, np. \InvalidArgumentException lub \RuntimeException). Dziedziczy getMessage(), getCode() i inne metody.
Który catch zostanie wykonany dla: throw new \InvalidArgumentException('błąd')?
PHP wykonuje pierwszy catch, którego typ pasuje do rzuconego wyjątku. InvalidArgumentException rozszerza LogicException, która rozszerza Exception — więc pasowałyby wszystkie trzy typy. Dlatego najbardziej szczegółowy catch powinien być pierwszy.
Ćwiczenie
Napisz klasę Dzielnik z metodą statyczną podziel(float $a, float $b): float. Gdy $b === 0.0, rzuć DivisionByZeroException (własna klasa dziedzicząca po \RuntimeException) z komunikatem 'Nie można dzielić przez zero'. Przetestuj przez try/catch dla poprawnego przypadku, dzielenia przez zero i bloku finally logującego 'Operacja zakończona'.
Pokaż rozwiązanie
<?php
declare(strict_types=1);
class DivisionByZeroException extends \RuntimeException {}
class Dzielnik
{
public static function podziel(float $a, float $b): float
{
if ($b === 0.0) {
throw new DivisionByZeroException('Nie można dzielić przez zero.');
}
return $a / $b;
}
}
// Test 1: poprawne dzielenie
try {
$wynik = Dzielnik::podziel(10.0, 2.0);
echo 'Wynik: ' . $wynik; // wynik: Wynik: 5
} catch (DivisionByZeroException $e) {
echo 'Błąd: ' . $e->getMessage();
} finally {
echo 'Operacja zakończona.'; // wynik: Operacja zakończona.
}
// Test 2: dzielenie przez zero
try {
$wynik = Dzielnik::podziel(10.0, 0.0);
echo 'Wynik: ' . $wynik;
} catch (DivisionByZeroException $e) {
echo 'Błąd: ' . $e->getMessage(); // wynik: Błąd: Nie można dzielić przez zero.
} finally {
echo 'Operacja zakończona.'; // wynik: Operacja zakończona.
}