W trakcie wykonywania skryptów, często okazuje się, że natrafiamy na błędy. W takiej sytuacji skrypt kończy swoje działanie, a istnieją sytuacje, w których nie jest to konieczne np. praca na nieistniejącym pliku.
I tutaj z pomocą przychodzą nam wyjątki(mechanizm obsługi błędów), które wyświetlają stosowną informację dla użytkownika w przypadku błędu i umożliwiają kontynuację pracy skryptu.
Mówimy, że wyjątki przechwytują błędy.
Wyobraźmy sobie funkcję zwracającą odwrotność podanej liczby (1/x). Co się stanie, gdy ktoś przekaże 0?
Jak zabezpieczyć taki kod?
Przykład z nieobsłużonym błędem:
<?php
function odwrotnosc($x) {
if (!$x) {
return "Błąd: dzielenie przez 0"; // Zwracamy string
}
return 1 / $x; // Zwracamy liczbę
}
// Użycie - PROBLEMATYCZNE!
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
// Wynik: Odwrotność 5 to 0.2
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>';
// Wynik: Odwrotność 0 to Błąd: dzielenie przez 0
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>';
// Wynik: Odwrotność 7 to 0.142857...
echo 'Skrypt kontynuuje swoją pracę';
A teraz rozwiązanie z obsługą błędów:
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0'); // Rzuć wyjątek
}
return 1 / $x;
}
// ============================================
// Przykład BEZ przechwycenia błędu - ZATRZYMA SKRYPT
// ============================================
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>'; // FATAL ERROR!
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>'; // Nie wykona się
echo 'Skrypt kontynuuje swoją pracę'; // Nie wykona się
Wynik w przeglądarce:
Odwrotność 5 to 0.2
Fatal error: Uncaught Exception: Dzielenie przez 0 in /path/to/file.php:4
Stack trace:
#0 /path/to/file.php(20): odwrotnosc(0)
#1 {main}
thrown in /path/to/file.php on line 4
Skrypt się zatrzymał! To zachowanie domyślne – jeśli wyjątek nie zostanie przechwycony, PHP przerywa działanie.
Z przechwyceniem wyjątku – POPRAWNIE
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0');
}
return 1 / $x;
}
// Przykład z przechwyconym błędem
try {
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>'; // Wyjątek!
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>'; // Nie wykona się
} catch (Exception $e) {
echo 'Przechwycony wyjątek: ' . $e->getMessage() . '<br>';
}
echo 'Skrypt kontynuuje swoją pracę';
Wynik:
Odwrotność 5 to 0.2
Przechwycony wyjątek: Dzielenie przez 0
Skrypt kontynuuje swoją pracę
Podstawowa składnia
Kluczowe słowa: throw, try, catch
Wyjątki są obiektami klasy Exception, które posiadają własny komunikat i kod błędu. Mogą one zostać wyłapane (try), rzucone (throw) i przechwycone (catch).
<?php
// throw - rzuć wyjątek
throw new Exception('Komunikat błędu');
// try - spróbuj wykonać kod
try {
// Kod, który może rzucić wyjątek
}
// catch - przechwyć wyjątek
catch (Exception $e) {
// Obsłuż błąd
}
throw – rzucanie wyjątku
<?php
function podziel($a, $b) {
if ($b == 0) {
throw new Exception('Nie można dzielić przez zero');
}
return $a / $b;
}
throw – „rzuć wyjątek”, przerwij normalne działanie i przekaż kontrolę do catch
try – próba wykonania kodu
<?php
try {
// Kod, który może generować błędy
echo podziel(10, 2); // OK
echo podziel(10, 0); // Wyjątek!
echo podziel(10, 5); // Nie wykona się
}
try – „spróbuj wykonać ten kod, ale przygotuj się na wyjątki”
catch – przechwycenie wyjątku
<?php
try {
echo podziel(10, 0);
} catch (Exception $e) {
echo 'Błąd: ' . $e->getMessage();
}
catch – „jeśli w try wystąpi wyjątek, wykonaj ten kod”
Schemat działania
┌─────────────────────────────────────────┐
│ TRY (spróbuj) │
│ ┌─────────────────────────────────┐ │
│ │ echo odwrotnosc(5); │ │
│ │ echo odwrotnosc(0); ───► throw │ │
│ │ echo odwrotnosc(7); │ │
│ └─────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ CATCH (przechwyć) │
│ ┌─────────────────────────────────┐ │
│ │ echo "Błąd: " . $e->getMessage()│ │
│ └─────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
│
▼
Kontynuacja skryptu
MOŻLIWE METODY WYJĄTKÓW
Wyjątki są obiektami klasy Exception, które posiadają przydatne metody diagnostyczne.
Wszystkie metody
<?php
try {
odwrotnosc(0);
} catch (Exception $e) {
echo "INFORMACJE O WYJĄTKU:\n\n";
// 1. Wiadomość błędu
echo "getMessage(): " . $e->getMessage() . "\n";
// Dzielenie przez 0
// 2. Kod błędu (domyślnie 0, można ustawić własny)
echo "getCode(): " . $e->getCode() . "\n";
// 0
// 3. Plik, w którym wystąpił błąd
echo "getFile(): " . $e->getFile() . "\n";
// /var/www/html/test.php
// 4. Linia w pliku
echo "getLine(): " . $e->getLine() . "\n";
// 4
// 5. Ślad wywołań (array) - backtrace
echo "getTrace():\n";
print_r($e->getTrace());
// 6. Ślad wywołań jako string (czytelniejszy)
echo "getTraceAsString():\n";
echo $e->getTraceAsString() . "\n";
// 7. Poprzedni wyjątek (jeśli istnieje)
echo "getPrevious(): ";
var_dump($e->getPrevious());
// NULL (brak poprzedniego)
}
Tabela metod Exception
| Metoda | Zwraca | Przykład | Zastosowanie |
|---|---|---|---|
getMessage() | string | „Dzielenie przez 0” | Komunikat dla użytkownika |
getCode() | int | 0 lub własny kod | Kategoryzacja błędów |
getFile() | string | „test.php” | Debugowanie |
getLine() | int | 4 | Debugowanie |
getTrace() | array | […] | Zaawansowane debugowanie |
getTraceAsString() | string | „#0 test.php(10)…” | Logowanie błędów |
getPrevious() | Exception|null | null | Łańcuch błędów |
Przykład praktyczny z wszystkimi metodami
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0', 100); // z kodem błędu
}
return 1 / $x;
}
try {
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>';
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>';
} catch (Exception $e) {
echo "<pre>";
echo "Przechwycony wyjątek:\n";
echo "• Wiadomość: " . $e->getMessage() . "\n";
echo "• Kod: " . $e->getCode() . "\n";
echo "• Plik: " . basename($e->getFile()) . "\n";
echo "• Linia: " . $e->getLine() . "\n";
echo "\nŚlad wywołań:\n" . $e->getTraceAsString();
echo "</pre>";
}
echo 'Skrypt kontynuuje swoją pracę';
Wynik:
Odwrotność 5 to 0.2
Przechwycony wyjątek:
• Wiadomość: Dzielenie przez 0
• Kod: 100
• Plik: test.php
• Linia: 4
Ślad wywołań:
#0 test.php(12): odwrotnosc(0)
#1 {main}
Skrypt kontynuuje swoją pracę
FINALLY
Finally służy do określenia kodu, który będzie wykonywany zawsze, niezależnie od wykrycia wyjątku.
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0');
}
return 1 / $x;
}
// Przykład 1: BEZ wyjątku
try {
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
} catch (Exception $e) {
echo 'Błąd: ' . $e->getMessage() . '<br>';
} finally {
echo 'Blok finally wykonany (BEZ wyjątku)<br><br>';
}
// Przykład 2: Z wyjątkiem
try {
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>';
} catch (Exception $e) {
echo 'Błąd: ' . $e->getMessage() . '<br>';
} finally {
echo 'Blok finally wykonany (Z wyjątkiem)<br><br>';
}
echo 'Skrypt kontynuuje działanie';
Wynik:
Odwrotność 5 to 0.2
Blok finally wykonany (BEZ wyjątku)
Błąd: Dzielenie przez 0
Blok finally wykonany (Z wyjątkiem)
Skrypt kontynuuje działanie
Należy jednak uważać podczas używania finally z return!
Jeżeli w bloku try… catch… zostanie wykryta instrukcja return, blok finally i tak zostanie wykonany. Dodatkowo, jeżeli blok finally też zawiera return, to zwrócona zostanie zawartość finally.
Problem: return w finally nadpisuje return z try lub catch!
<?php
//problematyczny kod
function test() {
try {
echo "try<br>";
return 1;
} catch (Exception $e) {
echo "catch<br>";
return 2;
} finally {
echo "finally<br>";
return 3; // TO NADPISZE poprzedni return!
}
return 4; // Nigdy się nie wykona
}
$wynik = test();
echo "Wynik: $wynik";
//zostanie wyświetlone
//try
//finally
//Wynik: 3
//poprawne rozwiązanie
function test() {
$result = null;
try {
$result = 1;
} catch (Exception $e) {
$result = 2;
} finally {
// Tylko cleanup, bez return
echo "Cleanup wykonany<br>";
}
return $result;
}
Praktyczne zastosowania finally
1. Zamykanie pliku
<?php
$plik = null;
try {
$plik = fopen('dane.txt', 'r');
if ($plik === false) {
throw new Exception("Nie można otworzyć pliku");
}
// Operacje na pliku...
$zawartosc = fread($plik, filesize('dane.txt'));
echo $zawartosc;
} catch (Exception $e) {
echo "Błąd: " . $e->getMessage();
} finally {
// ZAWSZE zamknij plik
if ($plik !== null) {
fclose($plik);
echo "Plik zamknięty<br>";
}
}
2. Zamykanie połączenia z bazą danych
<?php
$db = null;
try {
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
// Operacje na bazie...
$stmt = $db->query("SELECT * FROM users");
$users = $stmt->fetchAll();
} catch (PDOException $e) {
echo "Błąd bazy: " . $e->getMessage();
} finally {
// ZAWSZE zamknij połączenie
$db = null;
echo "Połączenie zamknięte<br>";
}
3. Zwalnianie zasobów
<?php
function processLargeFile($filename) {
$resource = acquireResource(); // Zdobądź zasób
try {
// Przetwarzanie...
if (!file_exists($filename)) {
throw new Exception("Plik nie istnieje");
}
// Długa operacja...
processFile($filename, $resource);
} catch (Exception $e) {
echo "Błąd: " . $e->getMessage();
} finally {
// ZAWSZE zwolnij zasób
releaseResource($resource);
echo "Zasób zwolniony<br>";
}
}
TWORZENIE WŁASNYCH WYJĄTKÓW
Wyjątki możemy tworzyć wg własnego uznania. Wystarczy, że stworzymy klasę dziedziczącą po klasie Exception.
<?php
class MojWyjatek extends Exception {
public function __construct() {
parent::__construct("To jest mój autorski wyjątek");
}
}
function testMojWyjatek() {
throw new MojWyjatek();
}
try {
testMojWyjatek();
} catch (MojWyjatek $e) {
echo "Błąd: " . $e->getMessage();
}
// Wynik: Błąd: To jest mój autorski wyjątek
Własny wyjątek z dodatkowymi danymi
<?php
class DivisionByZeroException extends Exception {
private $dividend;
public function __construct($dividend) {
$this->dividend = $dividend;
parent::__construct("Nie można podzielić $dividend przez 0");
}
public function getDividend() {
return $this->dividend;
}
}
function odwrotnosc($x) {
if (!$x) {
throw new DivisionByZeroException($x);
}
return 1 / $x;
}
try {
echo odwrotnosc(0);
} catch (DivisionByZeroException $e) {
echo "Błąd: " . $e->getMessage() . "<br>";
echo "Próbowano podzielić: " . $e->getDividend();
}
Hierarchia własnych wyjątków
<?php
// Bazowy wyjątek aplikacji
class AppException extends Exception {}
// Wyjątki matematyczne
class MathException extends AppException {}
class DivisionByZeroException extends MathException {}
class NegativeNumberException extends MathException {}
// Wyjątki plików
class FileException extends AppException {}
class FileNotFoundException extends FileException {}
class FileNotReadableException extends FileException {}
// ============================================
// UŻYCIE
// ============================================
function odwrotnosc($x) {
if (!$x) {
throw new DivisionByZeroException('Dzielenie przez 0');
}
if ($x < 0) {
throw new NegativeNumberException('Liczba ujemna: ' . $x);
}
return 1 / $x;
}
try {
echo odwrotnosc(-5);
} catch (DivisionByZeroException $e) {
echo "Dzielenie przez zero!";
} catch (NegativeNumberException $e) {
echo "Liczba ujemna: " . $e->getMessage();
} catch (MathException $e) {
echo "Błąd matematyczny: " . $e->getMessage();
} catch (AppException $e) {
echo "Błąd aplikacji: " . $e->getMessage();
}
Przykład praktyczny – walidacja formularza
<?php
class ValidationException extends Exception {
private array $errors;
public function __construct(array $errors, string $message = "Błędy walidacji") {
parent::__construct($message);
$this->errors = $errors;
}
public function getErrors(): array {
return $this->errors;
}
}
function validateUser(array $data): void {
$errors = [];
if (empty($data['email'])) {
$errors[] = 'Email jest wymagany';
}
if (empty($data['password'])) {
$errors[] = 'Hasło jest wymagane';
}
if (strlen($data['password'] ?? '') < 8) {
$errors[] = 'Hasło musi mieć min. 8 znaków';
}
if (!empty($errors)) {
throw new ValidationException($errors);
}
}
// Użycie
try {
validateUser(['email' => '', 'password' => 'short']);
} catch (ValidationException $e) {
echo "Błędy walidacji:<br>";
foreach ($e->getErrors() as $error) {
echo "• $error<br>";
}
}
//Wynik:
//Błędy walidacji:
//• Email jest wymagany
//• Hasło musi mieć min. 8 znaków
Przykłady praktyczne
1. System logowania błędów
<?php
class ErrorLogger {
private string $logFile = 'errors.log';
public function log(Exception $e): void {
$timestamp = date('Y-m-d H:i:s');
$logEntry = sprintf(
"[%s] %s: %s\nPlik: %s:%d\nŚlad:\n%s\n%s\n",
$timestamp,
get_class($e),
$e->getMessage(),
basename($e->getFile()),
$e->getLine(),
$e->getTraceAsString(),
str_repeat('-', 80)
);
file_put_contents($this->logFile, $logEntry, FILE_APPEND);
}
}
// Użycie
$logger = new ErrorLogger();
try {
odwrotnosc(0);
} catch (Exception $e) {
$logger->log($e);
echo "Wystąpił błąd. Szczegóły w logu.";
}
2. Kalkulator z obsługą błędów
<?php
class CalculatorException extends Exception {}
class DivisionByZeroException extends CalculatorException {}
class InvalidOperationException extends CalculatorException {}
class Calculator {
public function calculate(float $a, string $op, float $b): float {
switch ($op) {
case '+':
return $a + $b;
case '-':
return $a - $b;
case '*':
return $a * $b;
case '/':
if ($b == 0) {
throw new DivisionByZeroException("Nie można dzielić przez 0");
}
return $a / $b;
default:
throw new InvalidOperationException("Nieznana operacja: $op");
}
}
}
// Użycie
$calc = new Calculator();
try {
echo $calc->calculate(10, '/', 0);
} catch (DivisionByZeroException $e) {
echo "Błąd dzielenia: " . $e->getMessage();
} catch (CalculatorException $e) {
echo "Błąd kalkulatora: " . $e->getMessage();
}
3. Upload plików z walidacją
<?php
class FileUploadException extends Exception {}
class FileUploader {
private string $uploadDir = 'uploads';
private array $allowedExt = ['jpg', 'png', 'pdf'];
private int $maxSize = 5242880; // 5MB
public function upload(array $file): string {
// Sprawdź błędy uploadu
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new FileUploadException($this->getErrorMessage($file['error']));
}
// Sprawdź rozmiar
if ($file['size'] > $this->maxSize) {
throw new FileUploadException("Plik za duży (max 5MB)");
}
// Sprawdź rozszerzenie
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $this->allowedExt)) {
throw new FileUploadException("Niedozwolone rozszerzenie: $ext");
}
// Zapisz
$filename = uniqid() . '.' . $ext;
$path = $this->uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $path)) {
throw new FileUploadException("Nie można zapisać pliku");
}
return $path;
}
private function getErrorMessage(int $code): string {
$errors = [
UPLOAD_ERR_INI_SIZE => 'Plik za duży',
UPLOAD_ERR_PARTIAL => 'Plik wysłany częściowo',
UPLOAD_ERR_NO_FILE => 'Nie wybrano pliku',
];
return $errors[$code] ?? 'Nieznany błąd';
}
}
// Użycie
if ($_FILES['file'] ?? false) {
$uploader = new FileUploader();
try {
$path = $uploader->upload($_FILES['file']);
echo "Plik przesłany: $path";
} catch (FileUploadException $e) {
echo "Błąd: " . $e->getMessage();
}
}
Zadanie 1:
Napisz funkcję dlugoscCiagu(), która:
- Przyjmuje string (lub null)
- Zwraca długość ciągu
- Rzuca wyjątek jeśli pusty lub null
Zadanie 2
Stwórz klasę BankAccount z metodami withdraw() i deposit() oraz obsługą wyjątków.
Zadanie 3
Napisz funkcję readConfig() odczytującą plik JSON z obsługą błędów.