12. PHP – wyjątki, try…catch…


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

MetodaZwracaPrzykładZastosowanie
getMessage()string„Dzielenie przez 0”Komunikat dla użytkownika
getCode()int0 lub własny kodKategoryzacja błędów
getFile()string„test.php”Debugowanie
getLine()int4Debugowanie
getTrace()array[…]Zaawansowane debugowanie
getTraceAsString()string„#0 test.php(10)…”Logowanie błędów
getPrevious()Exception|nullnullŁ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.