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


Szybki quiz – przypomnienie poprzedniej lekcji

Lekcja 11 – operacje na plikach. Odpowiedz z pamięci, bez zaglądania do kodu.

1. Czym różni się tryb 'w' od 'a' w fopen()? Który z nich skasuje dotychczasową zawartość pliku?

2. Co znajdzie się w pliku log.txt po wykonaniu tego kodu?

$plik = fopen('log.txt', 'w');
fwrite($plik, "A\n");
fclose($plik);

$plik = fopen('log.txt', 'a');
fwrite($plik, "B\n");
fclose($plik);

echo file_get_contents('log.txt');

3. Znajdź dwa błędy w tym fragmencie:

$plik = fopen('dane.txt', 'r');
while (!feof($plik)) {
    echo fgets($plik);
}

4. Co zwraca fopen(), gdy pliku nie ma, a tryb to 'r'? Jak poprawnie sprawdzić ten wynik?

5. Uzupełnij tak, żeby linia została dopisana, a nie nadpisała pliku:

file_put_contents('log.txt', "nowa linia\n", ____________);

6. Która wersja wczyta plik do tablicy linii bez znaków końca linii?

a) file($sciezka)
b) file($sciezka, FILE_IGNORE_NEW_LINES)
c) file_get_contents($sciezka)

7. $wiersz = fgetcsv($plik, 0, ';'); — co dokładnie zwróci ta funkcja po dojściu do końca pliku, a co dla pustej linii?

Odpowiedzi

1. 'w' otwiera do zapisu i kasuje dotychczasową zawartość (albo tworzy plik, jeśli go nie ma). 'a' też zapisuje, ale dopisuje na końcu i niczego nie kasuje.

2. Wypisze:

A
B

Pierwszy fopen() w trybie 'w' wyczyścił plik i zapisał A, drugi w trybie 'a' dopisał B na końcu.

3. Po pierwsze: brak fclose($plik); — plik zostaje otwarty. Po drugie: brak sprawdzenia, czy fopen() się udało — jeśli pliku nie ma, $plik będzie false i feof(false) skończy się błędem. Poprawnie: if ($plik === false) { ... } zaraz po otwarciu.

4. Zwraca false (plus ostrzeżenie PHP). Sprawdzamy przez if ($plik === false) — z potrójnym znakiem równości, bo poprawny uchwyt pliku też mógłby zostać potraktowany jak wartość logiczna.

5. FILE_APPEND.

6. b) — file($sciezka, FILE_IGNORE_NEW_LINES). Sam file() zostawia \n na końcu każdego elementu, a file_get_contents() zwraca jeden wielki string, nie tablicę.

7. Po końcu pliku fgetcsv() zwraca false. Dla pustej linii zwraca tablicę z jednym elementem null (czyli [null]) — dlatego w wczytajZadania() pustą linię trzeba było osobno pomijać, zamiast liczyć na false.


TaskQuest

Dodatek

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.

TaskQuest 1.2 — awaria to nie to samo co „brak zadań”

KARTA PRACY

Po Lekcji 11 zadania leżą w pliku data/zadania.csv. To wygodne, ale plik może zniknąć, ktoś może go źle zapisać albo uszkodzić jeden wiersz. A TaskQuest w takich sytuacjach albo pokazuje „Brak zadań.”, albo po cichu gubi zadania. Użytkownik nie ma pojęcia, że coś poszło nie tak.

Dziś TaskQuest uczy się zauważać awarię, przerwać to, co robi, i zareagować — zamiast udawać, że wszystko jest w porządku.

Cel lekcji

Nauczyć TaskQuest:

  • zgłaszać awarię przez throw new Exception('komunikat'),
  • przechwytywać ją w try / catch i odczytywać komunikat przez getMessage(),
  • sprzątać po sobie w bloku finally (np. zamykać plik nawet wtedy, gdy coś poszło nie tak),
  • odróżniać sytuację normalną (pusty plik = brak zadań) od awarii (pliku nie ma, wiersz jest uszkodzony),
  • pilnować danych „przy wejściu” — uszkodzony wiersz ma być zgłoszony w momencie wczytywania pliku, a nie dopiero w innej funkcji.

Efekt: gdy data/zadania.csv zniknie albo będzie uszkodzony, strona TaskQuest nadal działa (poziom, XP i streak są na swoim miejscu), a w sekcji zadań pokazuje konkretny komunikat o problemie zamiast fałszywego „Brak zadań.”.

Uwaga — co jest, a czego jeszcze nie ma:

  • Używamy wyłącznie wbudowanej klasy Exception. new Exception('...') traktuj na razie jak gotową formułkę: tworzy „kartkę z opisem awarii”. Czym dokładnie jest new, obiekt i klasa — Lekcja 15. Własne rodzaje wyjątków (class ... extends Exception) też pojawią się dopiero po Lekcji 15.
  • Komunikaty i ostrzeżenia, które PHP wypisuje samo (Warning, Fatal error), oraz zapisywanie błędów do logu systemowego — Lekcja 23. Na razie tylko je rozpoznajemy.
  • Zapis zadań z poziomu strony (formularz) — dalej Lekcja 13. zapiszZadania() nadal jest wywoływana tylko z kodu i z testów.
  • Dane grywalizacji ($xp, $streak, $dataOstatniejAktywnosci) nadal są zapisane na sztywno w index.php — to nadal symulacja.

Rozgrzewka — 4 pytania z Lekcji 11

Odpowiedz z pamięci, zanim zaczniesz:

  1. Który tryb fopen() od razu kasuje zawartość pliku i dlaczego używamy go w zapiszZadania()?
  2. Co zwraca fgetcsv() dla pustej linii w pliku i jak wczytajZadania() radziła sobie z tym w Lekcji 11?
  3. Po odczycie z pliku wykonane to tekst "1" albo "0". Jak zamieniamy go na wartość logiczną?
  4. W Teście 5 z Lekcji 11 zmieniłaś/zmieniłeś nazwę pliku z zadaniami i strona pokazała „Brak zadań.”. Dlaczego ten komunikat był nieprawdziwy?

1. Wracamy do naszego TaskQuest

Aplikacja czyta zadania z data/zadania.csv. Wszystkie zmienne i funkcje z Lekcji 11 zostają. Zmieniamy tylko to, jak funkcje plikowe zachowują się przy awarii.

Docelowa struktura projektu

taskquest/
├── index.php                        (zmiana: try/catch przy wczytywaniu zadań, komunikat o błędzie)
├── diagnostyka.php                  bez zmian
├── config/
│   └── config.php                   bez zmian
├── includes/
│   ├── header.php                   (zmiana: jedna nowa reguła CSS .blad)
│   ├── footer.php                   (zmiana: "TaskQuest 1.2")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         bez zmian
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            bez zmian
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           (zmiana: wyjątki + nowa funkcja sprawdzWierszZadania())
└── data/
    └── zadania.csv                  bez zmian

Nie dochodzi żaden nowy plik, więc diagnostyka.php nadal pokazuje 11 × [OK]. Celowo nie dodajemy do niej próby wczytania zadań: ta strona dołącza tylko funkcje_diagnostyczne.php, żeby działać nawet przy zepsutym projekcie (Lekcja 10).

Co gdzie trafi

PlikCo się zmienia
includes/funkcje_plikow.phpwczytajZadania() i zapiszZadania() rzucają wyjątki; nowa sprawdzWierszZadania()
index.phptry / catch wokół wczytania zadań, nowa zmienna $bladZadan, komunikat o błędzie
includes/header.phpklasa .blad do ramki z komunikatem
includes/footer.phpnumer wersji

Świadoma zmiana zachowania funkcji

To pierwsza lekcja, w której zmieniamy „umowę” istniejących funkcji: wczytajZadania() przy awarii zwracała [], a teraz będzie rzucać wyjątek; zapiszZadania() zwracała bool, a teraz nie będzie zwracać niczego (void) i też rzuci wyjątek. Robimy to celowo — właśnie o to prosił „Most do Lekcji 12” z poprzedniego materiału. Jest to bezpieczne, bo zapiszZadania() nie jest jeszcze wywoływana z żadnej strony aplikacji, a wczytajZadania() ma jedno miejsce użycia (index.php), które i tak będziemy poprawiać.

Przygotowanie (zrób to teraz)

  1. Zrób kopię zapasową całego katalogu taskquest/ z Lekcji 11 — np. jako taskquest_lekcja11/ (obok, nie w środku). Przyda się do testu regresji.
  2. Sprawdź, czy data/zadania.csv ma nagłówek i 5 zadań, a index.php i diagnostyka.php działają (11 × [OK]).
  3. Zrób kopię pliku data/zadania.csv poza katalogiem taskquest/ (np. na pulpicie, jako zadania_kopia.csv). Będziemy go psuć w testach i eksperymentach, a chcemy mieć do czego wrócić. Kopii nie trzymaj w data/ — sekcja „Pliki danych” w diagnostyka.php pokazałaby wtedy 2 pliki.

2. Problem w aplikacji

Kama uruchamia TaskQuest rano i widzi: „Brak zadań.”. Ucieszyła się na pół sekundy — a potem przypomniała sobie, że wczoraj przenosiła pliki i mogła coś przesunąć. W innym dniu ktoś przez pomyłkę skasował średnik w jednym wierszu i zadanie „Ćwiczenia z DateTime” po prostu zniknęło z listy. Bez słowa wyjaśnienia.

Zobacz to na własne oczy.

Próba 1 — brak pliku. Zmień nazwę data/zadania.csv na data/zadania_x.csv i odśwież index.php. Strona pisze: „Brak zadań.” — a prawda jest inna: TaskQuest nie ma skąd wczytać zadań. Przywróć nazwę.

Próba 2 — uszkodzony wiersz. W data/zadania.csv, w wierszu zadania o id 3, zamień pierwszy średnik (ten zaraz po 3) na spację. Odśwież stronę. Zobaczysz Wszystkie: 4 — zadanie zniknęło, a nic nie wskazuje, że to awaria. Przywróć średnik.

SytuacjaCo widzi użytkownik dziśCo powinno się stać
Plik nie istnieje„Brak zadań.”komunikat, że pliku nie ma
Wiersz jest uszkodzonyzadanie znika po cichukomunikat, w której linii jest problem
Plik jest pusty (tylko nagłówek)„Brak zadań.”„Brak zadań.” — tu wszystko gra
zapiszZadania() nie może otworzyć plikuzwraca false, nikt tego nie sprawdzazapis nie może się „udać” po cichu

Dlaczego to trudne do naprawienia zwykłym return? Bo funkcja wczytajZadania() wie, że jest problem, ale nie wie, co z nim zrobić: pokazać komunikat? zapisać w logu? przerwać stronę? To wie dopiero kod, który ją wywołał (index.php). Potrzebujemy więc sposobu, żeby funkcja mogła powiedzieć „nie dam rady, i oto dlaczego” w taki sposób, którego nie da się zignorować.

Potrzebujemy:

  1. sposobu na zgłoszenie awarii z dowolnego miejsca w kodzie,
  2. sposobu na przerwanie tego, co program właśnie robi,
  3. sposobu na przechwycenie awarii tam, gdzie wiadomo, co z nią zrobić.

To właśnie robią wyjątki.

3. Nowy mechanizm PHP: wyjątki

3.1 Dlaczego „zwróć błąd jako wynik” nie wystarcza

BRUDNOPIS. Utwórz w katalogu taskquest/ plik brudnopis.php — to plik do eksperymentów, nie część aplikacji; nie dołączamy go nigdzie z index.php. Cała sekcja 3 dzieje się w tym pliku, dopóki nie zaznaczono inaczej. Zaczniemy od małego przykładu z XP — funkcja dodaje nagrodę do punktów i pilnuje, żeby nagroda nie była ujemna:

<?php

function dodajXp(int $xp, int $nagroda)
{
    if ($nagroda < 0) {
        return "Błąd: ujemna nagroda";
    }

    return $xp + $nagroda;
}

echo "Nowe XP: " . dodajXp(50, -10);

Wynik: Nowe XP: Błąd: ujemna nagroda.

Program wypisał komunikat błędu jak zwykły wynik. Trzy problemy:

  • błąd wygląda jak dane — nic go nie odróżnia od poprawnego wyniku, więc łatwo go zapisać do zmiennej $xp i liczyć dalej,
  • wołający może go po prostu zignorować (tak jak ignorujemy false z zapiszZadania()),
  • funkcja dodająca XP nie powinna decydować, jak pokazać błąd użytkownikowi.

3.2 throw — zgłoszenie awarii

Nadal w brudnopis.php — zamień jego zawartość na:

<?php

function dodajXp(int $xp, int $nagroda): int
{
    if ($nagroda < 0) {
        throw new Exception("Nagroda XP nie może być ujemna.");
        echo "Tego nie zobaczysz.";
    }

    return $xp + $nagroda;
}

echo "Start<br>";
echo "Nowe XP: " . dodajXp(50, -10) . "<br>";
echo "Koniec<br>";

Wynik w przeglądarce:

Start

Fatal error: Uncaught Exception: Nagroda XP nie może być ujemna. in ...brudnopis.php:6
Stack trace:
#0 ...brudnopis.php(14): dodajXp()
#1 {main}
  thrown in ...brudnopis.php on line 6

Co się stało:

  • throw natychmiast przerywa funkcję. Linia echo "Tego nie zobaczysz." nie wykonała się. Funkcja nie zwróciła też żadnego wyniku — dlatego nie widać „Nowe XP:” ani „Koniec”.
  • new Exception("...") tworzy „kartkę z opisem awarii”. Tekst w nawiasie to komunikat.
  • Nikt wyjątku nie przechwycił, więc PHP zatrzymał cały skrypt i wypisał Fatal error: Uncaught Exception. To domyślne zachowanie: wyjątek, którego nikt nie obsłużył, kończy program.
  • Stack trace mówi, którą drogą doszliśmy do awarii: dodajXp() została wywołana z linii 14 głównego skryptu ({main}).

3.3 try i catch — przechwycenie

Żeby program nie kończył się przy każdej awarii, opakowujemy ryzykowny kod w try, a reakcję na awarię wpisujemy w catch:

try {
    echo "Nowe XP: " . dodajXp(50, -10) . "<br>";
    echo "Ta linia się nie wykona.<br>";
} catch (Exception $wyjatek) {
    echo "Nie udało się dodać XP: " . $wyjatek->getMessage() . "<br>";
}

echo "Program działa dalej.";

Wynik:

Nie udało się dodać XP: Nagroda XP nie może być ujemna.
Program działa dalej.

Schemat działania:

try {
    linia 1  ── wykonana
    linia 2  ── tu poleciał throw ──┐
    linia 3  ── POMINIĘTA           │
}                                   │
catch (Exception $wyjatek) {  <─────┘  PHP przeskakuje tutaj
    reakcja na awarię
}
dalszy ciąg programu  ── działa normalnie

Ważne szczegóły:

  • Gdy w try nic się nie stanie, blok catch jest po prostu pomijany.
  • Gdy wyjątek poleci, reszta bloku try jest pomijana — nawet jeśli była poprawna.
  • $wyjatek to zwykła zmienna, w której PHP zostawia „kartkę z opisem awarii”. Nazwa jest dowolna (w dokumentacji i przykładach z internetu zobaczysz najczęściej $e). Trzymamy się opisowych nazw, jak w całym TaskQuest.

3.4 Co można wyczytać z wyjątku

$wyjatek->getMessage() to nasz komunikat. Wyjątek zna też kilka informacji „dla programisty”:

MetodaZwracaDla kogo
getMessage()tekst komunikatuużytkownik (jeśli sami go napisaliśmy)
getCode()liczbę (domyślnie 0) — opcjonalnie podaną jako 2. argument: new Exception('tekst', 100)programista
getLine()numer linii, w której zrobiono throwprogramista
getFile()pełną ścieżkę pliku, w którym zrobiono throwprogramista
getTraceAsString()„drogę” wywołań funkcji jako tekstprogramista

Zasada: użytkownikowi pokazujemy tylko komunikat, który sami napisaliśmy. getFile() i ślad wywołań ujawniają układ katalogów na serwerze — to wiedza dla nas, nie dla odwiedzającego stronę. Dlatego komunikaty w TaskQuest nie zawierają ścieżek do plików. (Do tego wrócimy w lekcji o bezpieczeństwie, 22.)

3.5 Wyjątek „leci w górę”

Wyjątek nie musi być złapany w tej funkcji, w której go rzucono. Jeśli nikt go tam nie przechwyci, przelatuje do funkcji, która ją wywołała, potem do następnej — aż trafi na catch. Jeśli takiego nie ma, kończy program (tak jak w 3.2).

function sprawdzNagrode(int $nagroda): void
{
    if ($nagroda < 0) {
        throw new Exception("Nagroda XP nie może być ujemna.");
    }
}

function dodajXp(int $xp, int $nagroda): int
{
    sprawdzNagrode($nagroda);      // wyjątek przelatuje przez dodajXp()...
    return $xp + $nagroda;
}

try {
    echo dodajXp(50, -10);
} catch (Exception $wyjatek) {
    echo $wyjatek->getMessage();   // ...i ląduje tutaj
}

Wynik: Nagroda XP nie może być ujemna. — mimo że throw jest dwie funkcje „głębiej”. Wypisz w catch także $wyjatek->getTraceAsString() i zobacz, że ślad zawiera obie funkcje (numery linii i argumenty u Ciebie mogą wyglądać nieco inaczej).

To najważniejsza idea tej lekcji, którą zapamiętamy jako regułę:

Kto wykrywa problem — rzuca wyjątek. Kto wie, co z problemem zrobić — go łapie.

sprawdzNagrode() wie, że nagroda jest zła, ale nie wie, czy pokazać komunikat na stronie, czy zapisać go w pliku. wczytajZadania() wie, że pliku nie ma. index.php wie, że w tym miejscu strony da się wypisać ramkę z ostrzeżeniem.

3.6 finally — sprzątanie, które wykona się zawsze

Blok finally wykona się zawsze: gdy w try wszystko poszło dobrze, gdy poleciał wyjątek i został złapany, a nawet gdy w try jest return.

try {
    echo "Otwieram plik...<br>";
    // ... praca, która może rzucić wyjątek ...
} catch (Exception $wyjatek) {
    echo "Błąd: " . $wyjatek->getMessage() . "<br>";
} finally {
    echo "Zamykam plik.<br>";
}

Po co? Bo sprzątanie (zamknięcie pliku, zwolnienie zasobu) nie może zależeć od tego, czy akurat wszystko poszło dobrze. Jeśli wyjątek poleci w środku pętli czytającej plik, kod po pętli się nie wykona — w tym fclose(). W wczytajZadania() użyjemy właśnie tego.

Uwaga: nigdy nie wpisuj return do bloku finally. Taki return nadpisuje wynik zwrócony w try (sprawdzisz to w eksperymencie dodatkowym).

3.7 Kiedy rzucać wyjątek, a kiedy nie

Wyjątek to sygnał: „nie potrafię zrobić tego, o co mnie poproszono”. Nie służy do zwykłego rozgałęziania programu.

Sytuacja w TaskQuestWyjątek?Dlaczego
Plik z zadaniami jest pusty (tylko nagłówek)nieto normalny stan: „Brak zadań.” jest poprawną odpowiedzią
Plik z zadaniami nie istniejetakfunkcja nie może wykonać zadania
Wiersz CSV ma 5 pól zamiast 6takdane są uszkodzone, nie da się ich zinterpretować
Login nie przechodzi regexa w walidujLogin()nietrue / false to oczekiwana odpowiedź, nie awaria
Wszystkie zadania są wykonanenienormalny stan aplikacji

Dlatego walidujLogin() zostaje funkcją zwracającą bool, a pusty plik nie rzuca wyjątku.

3.8 PHP też rzuca wyjątki — i jest jeszcze Error

Wyjątki rzucają także wbudowane funkcje PHP. Sprawdź to nadal w brudnopis.php:

<?php
$termin = new DateTime('to nie jest data');

Zobaczysz Fatal error: Uncaught DateMalformedStringException: Failed to parse time string ... (w PHP starszym niż 8.3 to zwykły Exception — zasada ta sama). Nasza komunikatTerminu() używa DateTime — więc gdyby w data/zadania.csv wylądowała bzdura zamiast daty, strona zatrzymałaby się w zupełnie innym miejscu niż to, w którym problem powstał (w sekcji 6 zobaczysz to na własne oczy i naprawimy).

Druga ważna sprawa: w PHP są dwie „rodziny” problemów. Exception to awarie, które przewidujemy i obsługujemy (brak pliku, zły wiersz). Error (np. TypeError, gdy do funkcji z int przekażesz tekst "abc") to zwykle błąd w programie, który należy naprawić, a nie łapać. catch (Exception $wyjatek) nie przechwyci Error — sprawdzisz to w eksperymencie 4. (Obie rodziny łączy wspólna nazwa Throwable; do tego wrócimy w Lekcji 23.) W tej lekcji łapiemy tylko Exception.

Typowe błędy przy wyjątkach

  • Pusty catch (catch (Exception $wyjatek) { }) — awaria „znika”, a program jedzie dalej, jakby nic się nie stało. To najgorsza możliwość: wracamy do „cichych” błędów z Lekcji 11.
  • try opakowujący cały program. Trudno potem wiedzieć, co dokładnie zawiodło i co da się kontynuować.
  • Rzucanie wyjątku dla sytuacji normalnej (pusty plik, zły login) — wyjątek to awaria, nie zwykły if.
  • Pokazywanie użytkownikowi getFile() lub śladu wywołań — ujawnia ścieżki na serwerze.
  • return w finally — nadpisuje wynik z try.
  • Zapomniany throw — sam new Exception(...) bez throw nic nie robi.

4. SAMODZIELNIE — dodajXp() z obsługą awarii

BRUDNOPIS. To ostatnie ćwiczenie w brudnopis.php, zanim zaczniemy zmieniać sam projekt (sekcja 5). W brudnopis.php (zastąp poprzednią zawartość) napisz kod, który:

  1. definiuje dodajXp(int $xp, int $nagroda): int — dla ujemnej nagrody rzuca wyjątek z komunikatem Nagroda XP nie może być ujemna., w przeciwnym razie zwraca sumę,
  2. ustawia $xp = 50 i dla każdej nagrody z listy [20, 0, -5, 15] (pętla foreach) próbuje wywołać dodajXp(),
  3. wypisuje dla każdej nagrody jedną linię: albo Nagroda 20 XP: razem 70, albo Nagroda -5 XP: odrzucona — <komunikat wyjątku>,
  4. pod pętlą wypisuje Koniec sprawdzania.

Efekt w przeglądarce:

Nagroda 20 XP: razem 70
Nagroda 0 XP: razem 50
Nagroda -5 XP: odrzucona — Nagroda XP nie może być ujemna.
Nagroda 15 XP: razem 65
Koniec sprawdzania

Jak sprawdzić: po odrzuconej nagrodzie -5 program nie może się zatrzymać — nagroda 15 musi zostać przetworzona.

Rozszerzenie: dodaj blok finally, który po każdej nagrodzie (niezależnie od wyniku) wypisze (sprawdzono nagrodę 20), (sprawdzono nagrodę 0) itd. Przewidź, w jakiej kolejności pojawią się linie dla nagrody -5, zanim uruchomisz kod.

HINT

Pomyśl, gdzie ma stać try: wokół całej pętli czy wewnątrz niej? Program ma przetworzyć nagrodę 15 po odrzuceniu -5 — więc wyjątek musi zostać złapany w obrębie jednego obrotu pętli.

POMOC
foreach ($nagrody as $nagroda) {
    try {
        // wywołanie dodajXp() i wypisanie wyniku
    } catch (Exception $wyjatek) {
        // wypisanie informacji o odrzuceniu + $wyjatek->getMessage()
    } finally {
        // (tylko w rozszerzeniu)
    }
}
ROZWIĄZANIE
<?php

function dodajXp(int $xp, int $nagroda): int
{
    if ($nagroda < 0) {
        throw new Exception("Nagroda XP nie może być ujemna.");
    }

    return $xp + $nagroda;
}

$xp = 50;
$nagrody = [20, 0, -5, 15];

foreach ($nagrody as $nagroda) {
    try {
        $razem = dodajXp($xp, $nagroda);
        echo "Nagroda $nagroda XP: razem $razem<br>";
    } catch (Exception $wyjatek) {
        echo "Nagroda $nagroda XP: odrzucona — " . $wyjatek->getMessage() . "<br>";
    } finally {
        echo "(sprawdzono nagrodę $nagroda)<br>";
    }
}

echo "Koniec sprawdzania";

Dla nagrody -5 kolejność jest taka: najpierw komunikat z catch, potem (sprawdzono nagrodę -5) z finally. Jeśli nie chcesz linii z finally, usuń cały ten blok.

5. Włączamy wyjątki do TaskQuest — krok po kroku

PROJEKT. Od tego miejsca kończymy pracę w brudnopis.php i zmieniamy pliki prawdziwej aplikacji w katalogu taskquest/ — dokładnie te, które są wymienione przy każdym kroku niżej. Zmieniamy cztery rzeczy, w tej kolejności: najpierw funkcje, które zgłaszają awarie, potem miejsce, które je obsługuje. Dlatego po Krokach 1 i 2, a przed Krokiem 3, strona index.php przy awarii pliku pokaże Fatal error — to normalne, bo jeszcze nikt awarii nie łapie.

Krok 1 — SAMODZIELNIE: wczytajZadania() zgłasza kłopoty z plikiem

Plik projektu: includes/funkcje_plikow.php. Zamień w wczytajZadania() trzy miejsca, w których dotąd było return $zadania; (plik nie jest plikiem, plik nie jest czytelny, fopen() zwróciło false), na rzucenie wyjątku z komunikatem:

PrzypadekKomunikat (dokładnie taki)
!is_file($sciezka)Plik z zadaniami nie istnieje.
!is_readable($sciezka)Brak uprawnień do odczytu pliku z zadaniami.
$uchwyt === falseNie udało się otworzyć pliku z zadaniami.

Zauważ, że linia $zadania = []; nie musi już stać na początku funkcji — przenieś ją pod sprawdzenia, tuż przed odczytem pliku.

Jak sprawdzić — wracamy na chwilę do brudnopisu. W brudnopis.php:

<?php

require_once __DIR__ . '/includes/funkcje_plikow.php';

try {
    wczytajZadania(__DIR__ . '/data/nie_ma.csv');
} catch (Exception $wyjatek) {
    echo $wyjatek->getMessage();
}

Ma się wypisać: Plik z zadaniami nie istnieje.

HINT

Nie potrzebujesz else. throw przerywa funkcję dokładnie tak jak wcześniej robił to return, więc reszta funkcji wykona się tylko wtedy, gdy wszystko jest w porządku.

ROZWIĄZANIE

Początek funkcji po zmianie:

function wczytajZadania(string $sciezka): array
{
    if (!is_file($sciezka)) {
        throw new Exception("Plik z zadaniami nie istnieje.");
    }

    if (!is_readable($sciezka)) {
        throw new Exception("Brak uprawnień do odczytu pliku z zadaniami.");
    }

    $uchwyt = fopen($sciezka, 'r');

    if ($uchwyt === false) {
        throw new Exception("Nie udało się otworzyć pliku z zadaniami.");
    }

    $zadania = [];

    // ... dalej bez zmian (na razie) ...

Krok 2 — SAMODZIELNIE: uszkodzony wiersz i finally

Plik projektu: includes/funkcje_plikow.php. Dziś uszkodzony wiersz (count($wiersz) !== 6) jest pomijany przez continue — po cichu. Zmieniamy to.

a) Nad wczytajZadania() dopisz nową funkcję:

function sprawdzWierszZadania(array $wiersz, int $numerLinii): void

Na razie ma tylko jedno sprawdzenie: jeśli wiersz nie ma dokładnie 6 pól — rzuca wyjątek Linia N: oczekiwano 6 pól, a jest X. (np. Linia 4: oczekiwano 6 pól, a jest 5.). Funkcja niczego nie zwraca (void) — albo wiersz jest w porządku i nic się nie dzieje, albo leci wyjątek.

b) W wczytajZadania():

  1. usuń dotychczasowy fragment if (count($wiersz) !== 6) { continue; } — to on po cichu gubił zadania,
  2. numeruj linie pliku: nagłówek to linia 1, więc pierwsze zadanie jest w linii 2 (zmienna $numerLinii),
  3. pustą linię (fgetcsv() zwraca wtedy tablicę [null]) nadal pomijaj przez continue — to niegroźne i nie jest awarią,
  4. dla pozostałych wierszy wywołaj sprawdzWierszZadania(),
  5. cały odczyt (nagłówek + pętla while) umieść w try { ... } finally { fclose($uchwyt); }.

Dlaczego finally? Bo sprawdzWierszZadania() może rzucić wyjątek w środku pętli, a wtedy kod po pętli (w tym dotychczasowe fclose()) w ogóle się nie wykona. finally gwarantuje zamknięcie pliku w każdej sytuacji. Uczciwie: PHP i tak zamyka otwarte pliki, gdy skrypt się kończy, więc w naszej małej aplikacji nie zobaczysz różnicy gołym okiem. Ale funkcja może być wywoływana wiele razy w jednym skrypcie, w Windows otwartego pliku nie da się usunąć ani przenieść, a nawyk „sprzątam zawsze” będzie kluczowy przy bazie danych (od Lekcji 17). Zauważ też, że return $zadania; zostaje poza try/finally.

HINT

Pusta linia to [null] — tablica z jednym elementem null. Porównaj ją przez === z takim właśnie zapisem. Numer linii zwiększaj na początku każdego obrotu pętli, zanim cokolwiek sprawdzisz.

POMOC
    $zadania = [];

    try {
        fgetcsv($uchwyt, 0, ';', '"', '');   // nagłówek
        $numerLinii = 1;

        while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
            $numerLinii++;

            // pusta linia? -> continue

            // sprawdzenie wiersza

            $zadania[] = [ /* jak dotąd */ ];
        }
    } finally {
        // zamknięcie pliku
    }

    return $zadania;
ROZWIĄZANIE
function sprawdzWierszZadania(array $wiersz, int $numerLinii): void
{
    if (count($wiersz) !== 6) {
        throw new Exception("Linia $numerLinii: oczekiwano 6 pól, a jest " . count($wiersz) . ".");
    }
}


function wczytajZadania(string $sciezka): array
{
    // ... trzy sprawdzenia z Kroku 1 bez zmian ...

    $zadania = [];

    try {
        fgetcsv($uchwyt, 0, ';', '"', '');   // pierwszy wiersz to nagłówek — pomijamy go
        $numerLinii = 1;

        while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
            $numerLinii++;

            if ($wiersz === [null]) {
                continue;   // pusta linia — niegroźna, pomijamy ją jak w Lekcji 11
            }

            sprawdzWierszZadania($wiersz, $numerLinii);

            $zadania[] = [
                'id' => (int) $wiersz[0],
                'tytul' => $wiersz[1],
                'opis' => $wiersz[2],
                'termin' => $wiersz[3],
                'wykonane' => $wiersz[4] === '1',
                'xp' => (int) $wiersz[5]
            ];
        }
    } finally {
        fclose($uchwyt);   // wykona się zawsze — także wtedy, gdy wyżej poleciał wyjątek
    }

    return $zadania;
}

Numer linii liczymy jako numer wiersza pliku (nagłówek = 1). To wystarcza, dopóki żadne pole nie zawiera znaku nowej linii w środku — u nas tak jest.

Krok 3 — SAMODZIELNIE: index.php łapie awarię

Pliki projektu: index.php, includes/header.php, includes/footer.php. Teraz miejsce, które wie, co robić. TaskQuest ma nie zatrzymywać się przy awarii pliku: poziom, XP, pasek i streak mają działać jak zawsze, a w sekcji „Twoje zadania” ma pojawić się komunikat o problemie.

W index.php:

  1. Blok „dane zadań (z pliku CSV)” ma zawsze ustawić $zadania (na początek pusta tablica) oraz nową zmienną $bladZadan: null, gdy wszystko poszło dobrze, a w przeciwnym razie tekst z wyjątku. Wczytanie zadań opakuj w try / catch.
  2. Pod nagłówkiem <h2>Twoje zadania</h2>: gdy $bladZadan !== null — wypisz <p class="blad">Nie udało się wczytać zadań: ...</p> i nic więcej z tej sekcji (bez liczników, komunikatów i listy). W przeciwnym razie sekcja wygląda dokładnie jak dotąd.
  3. W includes/header.php dodaj do <style> regułę dla klasy .blad.
  4. W includes/footer.php zmień TaskQuest 1.1 na TaskQuest 1.2.

Zastanów się: dlaczego $zadania musi mieć wartość początkową przed try? Co by się stało z count($zadania) niżej w pliku, gdyby wyjątek poleciał, zanim cokolwiek do $zadania trafiło?

HINT

Fragment strony, który ma się pojawić tylko w jednym z dwóch przypadków, możesz objąć tak: <?php if (warunek) { ?> … HTML … <?php } else { ?> … HTML i PHP … <?php } ?>. Wchodzenie i wychodzenie z PHP znasz z header.php i z index.php.

POMOC

Blok danych zadań:

$zadania = [];
$bladZadan = null;

try {
    // wczytanie
} catch (Exception $wyjatek) {
    // zapamiętanie komunikatu
}

Sekcja zadań:

        <h2>Twoje zadania</h2>

        <?php if ($bladZadan !== null) { ?>
        <p class="blad">... <?= $bladZadan; ?></p>
        <?php } else { ?>

        ... cała dotychczasowa zawartość sekcji (liczniki, blok PHP z listą) ...

        <?php } ?>
ROZWIĄZANIE

a) index.php — blok danych zadań:

// --- dane zadań (z pliku CSV) ---
$zadania = [];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek->getMessage();
}

b) index.php — pod nagłówkiem sekcji zadań, przed akapitem z licznikami, dodaj początek konstrukcji if / else (cała dotychczasowa reszta sekcji zostaje w środku else):

        <h2>Twoje zadania</h2>

        <?php if ($bladZadan !== null) { ?>
        <p class="blad">Nie udało się wczytać zadań: <?= $bladZadan; ?></p>
        <?php } else { ?>

        <p>
            Wszystkie: ...

c) index.php — koniec pliku: po zamykającym ?> bloku z pętlą foreach dodaj jedną linię, a dopiero potem dołączasz footer:

        ?>
        <?php } ?>
<?php
require_once __DIR__ . '/includes/footer.php';

d) includes/header.php — w <style>, pod .zadanie.wykonane:

.blad { border: 1px solid #c00; background: #fdecec; color: #900; border-radius: 8px; padding: 8px 16px; }

e) includes/footer.php: <p>TaskQuest 1.2</p>.

Na koniec usuń nadmiarowe znaczniki php <?php ?>

Odpowiedź na pytanie: gdyby wyjątek poleciał w środku wczytajZadania(), przypisanie $zadania = ... w ogóle by się nie wykonało. Bez wartości początkowej [] zmienna $zadania nie istniałaby, a count($zadania) niżej w pliku wywołałoby kolejny błąd.

Sprawdź: zmień nazwę data/zadania.csv na data/zadania_x.csv i odśwież stronę. Poziom, XP, pasek i streak działają, a w sekcji zadań jest czerwona ramka: Nie udało się wczytać zadań: Plik z zadaniami nie istnieje. Przywróć nazwę pliku.

Uwaga: przy awarii $zadania jest puste, więc $zadanieWykonane (uproszczenie z Lekcji 9) wychodzi false i streak jest liczony tak, jakby dziś nie wykonano żadnego zadania. Świadomie zostawiamy to uproszczenie: streak nadal jest symulacją, a jego prawdziwe źródło (aktywność w bazie) pojawi się dopiero razem z bazą danych.

Krok 4 — SAMODZIELNIE: zapiszZadania() nie „udaje”, że zapisała

Plik projektu: includes/funkcje_plikow.php. Dotąd zapiszZadania() zwracała false, gdy nie mogła otworzyć pliku — i nikt tego nie sprawdzał. Wyjątku nie da się zignorować tak łatwo jak zwróconego false, więc zmieniamy tę funkcję tak samo jak wczytajZadania().

Zmień zapiszZadania():

  1. typ zwracany z bool na void,
  2. gdy fopen() zwróci false — rzuć wyjątek Nie udało się zapisać pliku z zadaniami.,
  3. usuń return true; z końca funkcji (funkcja void niczego nie zwraca).
ROZWIĄZANIE
function zapiszZadania(string $sciezka, array $zadania): void
{
    $uchwyt = fopen($sciezka, 'w');

    if ($uchwyt === false) {
        throw new Exception("Nie udało się zapisać pliku z zadaniami.");
    }

    // ... nagłówek i pętla foreach bez zmian ...

    fclose($uchwyt);
}

Sprawdzimy ją w Teście 2. Zwróć uwagę na jedną rzecz, której nie robimy: jeśli zapis urwie się w połowie (np. skończy się miejsce na dysku), plik zostanie niepełny — wyjątek dotyczy tylko fopen(). Bezpieczniejsze zapisywanie to temat na później.

6. Kolejne zadanie — sprawdzamy zawartość wiersza, a nie tylko liczbę pól

Wiersz z 6 polami może być i tak zepsuty. Eksperyment przed poprawką (plik danych projektu data/zadania.csv — to tymczasowa zmiana, na końcu ją cofniesz): w wierszu zadania o id 4 zamień termin 2026-09-25 na jutro i odśwież index.php.

Zobaczysz Fatal error: Uncaught DateMalformedStringException ... Failed to parse time string (jutro) — i to z funkcji komunikatTerminu(), czyli z miejsca, które nie ma nic wspólnego z odczytem pliku. Przy okazji: wyjątek poleciał poza naszym try, więc strona przestała działać. Wniosek: złe dane należy zatrzymać przy wejściu, w funkcji, która czyta plik — wtedy komunikat mówi wprost, która linia jest zła. Przywróć termin.

SAMODZIELNIE — plik projektu: includes/funkcje_plikow.php. Rozbuduj sprawdzWierszZadania() o trzy kolejne sprawdzenia (po sprawdzeniu liczby pól). Każde rzuca wyjątek z takim komunikatem:

Warunek awariiKomunikat (dokładnie taki)
termin nie ma kształtu RRRR-MM-DDLinia N: termin 'X' nie ma formatu RRRR-MM-DD.
wykonane to nie 0 i nie 1Linia N: pole wykonane musi mieć wartość 0 albo 1.
id albo xp nie jest liczbą całkowitą (same cyfry)Linia N: id i xp muszą być liczbami całkowitymi.

N to numer linii, X — wartość terminu z pliku (np. Linia 5: termin 'jutro' nie ma formatu RRRR-MM-DD.). Kształt daty i „same cyfry” sprawdzisz preg_match() — regexy znasz z Lekcji 8.

HINT

Data w formacie RRRR-MM-DD to: cztery cyfry, myślnik, dwie cyfry, myślnik, dwie cyfry — całość od początku do końca tekstu (pamiętaj o ^ i $). „Same cyfry” to jedna lub więcej cyfr, też od początku do końca.

POMOC
if (!preg_match('/^\d{4}-.../', $wiersz[3])) {
    throw new Exception("Linia $numerLinii: termin '{$wiersz[3]}' nie ma formatu RRRR-MM-DD.");
}

Wzorzec na „same cyfry” zapisz jako /^\d+$/ i sprawdź nim zarówno $wiersz[0], jak i $wiersz[5] (jeden if z operatorem ||).

ROZWIĄZANIE
function sprawdzWierszZadania(array $wiersz, int $numerLinii): void
{
    if (count($wiersz) !== 6) {
        throw new Exception("Linia $numerLinii: oczekiwano 6 pól, a jest " . count($wiersz) . ".");
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $wiersz[3])) {
        throw new Exception("Linia $numerLinii: termin '{$wiersz[3]}' nie ma formatu RRRR-MM-DD.");
    }

    if ($wiersz[4] !== '0' && $wiersz[4] !== '1') {
        throw new Exception("Linia $numerLinii: pole wykonane musi mieć wartość 0 albo 1.");
    }

    if (!preg_match('/^\d+$/', $wiersz[0]) || !preg_match('/^\d+$/', $wiersz[5])) {
        throw new Exception("Linia $numerLinii: id i xp muszą być liczbami całkowitymi.");
    }
}

Kolejność sprawdzeń ma znaczenie: dopiero po sprawdzeniu, że pól jest 6, wolno sięgać po $wiersz[3], $wiersz[4] i $wiersz[5].

Powtórz eksperyment z terminem jutro. Teraz strona pokazuje ramkę: Nie udało się wczytać zadań: Linia 5: termin 'jutro’ nie ma formatu RRRR-MM-DD. Przywróć termin.

Ograniczenie, o którym warto wiedzieć: wzorzec sprawdza kształt tekstu, nie kalendarz. Termin 2026-13-45 ma poprawny kształt, więc przejdzie — i dopiero DateTime w komunikatTerminu() się na nim wywróci. Wracamy do tego w Teście 1 i w zadaniu końcowym A.

7. Testy

Test 1 — co zwraca wczytajZadania() dla różnych plików

BRUDNOPIS. Przewidź wynik każdego przypadku, a dopiero potem sprawdź. W brudnopis.php:

<?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';

$plikTestowy = __DIR__ . '/data/test.csv';
$naglowek = "id;tytul;opis;termin;wykonane;xp\n";

function sprobuj(string $plik, string $zawartosc): string
{
    file_put_contents($plik, $zawartosc);

    try {
        return "OK: " . count(wczytajZadania($plik)) . " zadań";
    } catch (Exception $wyjatek) {
        return "BŁĄD: " . $wyjatek->getMessage();
    }
}

echo sprobuj($plikTestowy, "") . "<br>";
echo sprobuj($plikTestowy, $naglowek) . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n\n2;c;d;2026-01-02;0;7\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1\n") . "<br>";
echo sprobuj($plikTestowy, "id,tytul\n1,a\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;jutro;1;5\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;2;5\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;abc\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-13-45;1;5\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n2;c;d;2026-01-02;0;7\n3;e;f;2026-01-03\n") . "<br>";
echo sprobuj($plikTestowy, $naglowek . "   \n") . "<br>";

unlink($plikTestowy);

try {
    wczytajZadania(__DIR__ . '/data/nie_ma.csv');
} catch (Exception $wyjatek) {
    echo "BŁĄD: " . $wyjatek->getMessage();
}
Zawartość plikuOczekiwany wynik
pusty plik (0 bajtów)OK: 0 zadań
tylko nagłówekOK: 0 zadań
nagłówek + 1 poprawny wierszOK: 1 zadań
nagłówek + wiersz + pusta linia + wierszOK: 2 zadań
wiersz z 5 polamiBŁĄD: Linia 2: oczekiwano 6 pól, a jest 5.
plik rozdzielony przecinkamiBŁĄD: Linia 2: oczekiwano 6 pól, a jest 1.
termin jutroBŁĄD: Linia 2: termin 'jutro' nie ma formatu RRRR-MM-DD.
wykonane równe 2BŁĄD: Linia 2: pole wykonane musi mieć wartość 0 albo 1.
xp równe abcBŁĄD: Linia 2: id i xp muszą być liczbami całkowitymi.
termin 2026-13-45OK: 1 zadań (!) — kształt jest poprawny; to ograniczenie z sekcji 6
dwa poprawne wiersze i trzeci z 4 polamiBŁĄD: Linia 4: oczekiwano 6 pól, a jest 4.
linia zawierająca same spacjeBŁĄD: Linia 2: oczekiwano 6 pól, a jest 1. (to nie jest pusta linia!)
plik nie istniejeBŁĄD: Plik z zadaniami nie istnieje. (bez żadnego ostrzeżenia PHP)

Zwróć uwagę na różnicę względem Lekcji 11: tam wszystkie „BŁĄD” z tabeli dawały po prostu 0. Teraz pusty plik i uszkodzony plik to dwie różne odpowiedzi.

Test 2 — zapiszZadania() przy awarii

BRUDNOPIS. W brudnopis.php:

<?php

require_once __DIR__ . '/includes/funkcje_plikow.php';

try {
    zapiszZadania(__DIR__ . '/nie_ma_takiego_katalogu/test.csv', []);
    echo "Zapisano.";
} catch (Exception $wyjatek) {
    echo "BŁĄD: " . $wyjatek->getMessage();
}

Oczekiwane: ostrzeżenie PHP w stylu Warning: fopen(...): Failed to open stream: No such file or directory ... (to komunikat wypisany przez sam PHP — osobny mechanizm, do którego wrócimy w Lekcji 23) oraz nasza linia: BŁĄD: Nie udało się zapisać pliku z zadaniami. Katalog nie_ma_takiego_katalogu nie istnieje, więc fopen() w trybie w nie może utworzyć pliku.

Test 3 — zapis i odczyt nadal się zgadzają (regresja z Lekcji 11)

BRUDNOPIS. Plik data/zadania.php już nie istnieje, więc wzorcem jest prawdziwy data/zadania.csv:

<?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';

$plikTestowy = __DIR__ . '/data/test.csv';

$zadania = wczytajZadania(PLIK_ZADAN);
zapiszZadania($plikTestowy, $zadania);
$odczytane = wczytajZadania($plikTestowy);

var_dump($odczytane === $zadania);

unlink($plikTestowy);

Oczekiwane: bool(true) — w zapiszZadania() zmieniliśmy tylko obsługę awarii, nie sam zapis.

Test 4 — cała aplikacja

PLIK DANYCH PROJEKTU. Wszystkie zmiany rób bezpośrednio w data/zadania.csv (albo w jego nazwie) i po każdej przywracaj oryginał (z kopii z Przygotowania) — nic tu nie piszemy w brudnopisie.

ZmianaOczekiwany efekt na stronie
plik nazywa się zadania_x.csvpoziom, XP, pasek, streak działają; w sekcji zadań ramka: Nie udało się wczytać zadań: Plik z zadaniami nie istnieje.; brak liczników i listy
w wierszu zadania 3 pierwszy średnik zamieniony na spacjęramka: ... Linia 4: oczekiwano 6 pól, a jest 5.
w zadaniu 4 termin jutroramka: ... Linia 5: termin 'jutro' nie ma formatu RRRR-MM-DD.
w pliku tylko nagłówekbrak ramki, komunikat Brak zadań. (to nie awaria)
pusta linia między zadaniami 2 i 3strona wygląda normalnie, nadal 5 zadań
oryginalny plikstrona jak po Lekcji 11

Test 5 — regresja

Uruchom obok siebie taskquest_lekcja11/index.php i nowy taskquest/index.php tego samego dnia. Poza wersją w stopce (1.1 → 1.2) i jedną nową regułą CSS w <style> (niewidoczną, bo nic jej nie używa) strona ma być identyczna: te same liczniki, ten sam pasek, te same komunikaty terminów. diagnostyka.php nadal pokazuje 11 × [OK].

Sprzątanie

Gdy testy 1–5 przeszły:

  1. usuń brudnopis.php oraz ewentualne pozostałości data/test.csv,
  2. upewnij się, że data/zadania.csv ma oryginalną zawartość (5 zadań) i oryginalną nazwę,
  3. odśwież index.php i diagnostyka.php.

8. Obowiązkowe eksperymenty

Gdzie: Eksperymenty 1 i 4 wykonuj w brudnopis.php (nic nie zmieniasz w projekcie); Eksperymenty 2 i 3 wykonuj bezpośrednio w pliku projektu index.php — po każdym przywróć kod do stanu z Kroku 3.

Eksperyment 1: try wokół pętli

W rozwiązaniu z sekcji 4 przenieś try { ... } catch { ... } tak, żeby otaczał całą pętlę foreach. Uruchom. Które nagrody zostały przetworzone? Czy nagroda 15 się pojawiła? Wyjaśnij, dlaczego — i w którym z tych dwóch ustawień TaskQuest lepiej znosiłby jedno złe zadanie na liście.

Eksperyment 2: wyjątek bez catch

Plik projektu index.php. Zamień blok z try / catch z powrotem na samo $zadania = wczytajZadania(PLIK_ZADAN); i zmień nazwę pliku z zadaniami. Przeczytaj Fatal error: w której funkcji i w której linii wyjątek został rzucony? Które funkcje są na liście Stack trace? Jak myślisz — czy odwiedzający stronę powinien widzieć takie ścieżki? Przywróć try / catch i nazwę pliku.

Eksperyment 3: pusty catch

Plik projektu index.php. Zostaw w catch pustą treść:

} catch (Exception $wyjatek) {
}

i zmień nazwę pliku z zadaniami. Co pokazuje teraz strona? Z którą sytuacją z Lekcji 11 to się kojarzy i dlaczego jest gorsza niż Fatal error? Przywróć poprawny catch.

Eksperyment 4: Exception to nie wszystko

BRUDNOPIS. Użyj dodajXp() z sekcji 4 i wywołaj ją tak, żeby otoczyć wywołanie zwykłym try / catch (Exception $wyjatek):

try {
    echo dodajXp(50, "abc");
} catch (Exception $wyjatek) {
    echo "Złapano: " . $wyjatek->getMessage();
}

Czy „Złapano:” się pojawiło? Przeczytaj komunikat Fatal error — jaki to rodzaj błędu (Uncaught ...)? Dlaczego catch (Exception ...) go nie złapał? Czy to dobrze, że taki błąd zatrzymuje program?

Eksperyment 5 (dodatkowy): return w finally

BRUDNOPIS. Przewidź, co wypisze poniższy kod, a potem uruchom:

function test(): int
{
    try {
        return 1;
    } finally {
        return 3;
    }
}

echo test();

9. ROZWIĄZANIE

ROZWIĄZANIE

Pełny kod TaskQuest po Lekcji 12. Nowe lub zmienione: includes/funkcje_plikow.php, index.php, includes/header.php, includes/footer.php. Bez zmian względem Lekcji 11: config/config.php, includes/funkcje_grywalizacji.php, includes/funkcje_tekstowe.php, includes/funkcje_terminow.php, includes/funkcje_zadan.php, includes/funkcje_diagnostyczne.php, diagnostyka.php, data/zadania.csv.

includes/funkcje_plikow.php

<?php

// Funkcje odczytu i zapisu plików z danymi TaskQuest (CSV, separator ;)
// Od Lekcji 12 przy awarii rzucają wyjątki (throw new Exception), zamiast po cichu zwracać [] albo false.

function sprawdzWierszZadania(array $wiersz, int $numerLinii): void
{
    if (count($wiersz) !== 6) {
        throw new Exception("Linia $numerLinii: oczekiwano 6 pól, a jest " . count($wiersz) . ".");
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $wiersz[3])) {
        throw new Exception("Linia $numerLinii: termin '{$wiersz[3]}' nie ma formatu RRRR-MM-DD.");
    }

    if ($wiersz[4] !== '0' && $wiersz[4] !== '1') {
        throw new Exception("Linia $numerLinii: pole wykonane musi mieć wartość 0 albo 1.");
    }

    if (!preg_match('/^\d+$/', $wiersz[0]) || !preg_match('/^\d+$/', $wiersz[5])) {
        throw new Exception("Linia $numerLinii: id i xp muszą być liczbami całkowitymi.");
    }
}


function wczytajZadania(string $sciezka): array
{
    if (!is_file($sciezka)) {
        throw new Exception("Plik z zadaniami nie istnieje.");
    }

    if (!is_readable($sciezka)) {
        throw new Exception("Brak uprawnień do odczytu pliku z zadaniami.");
    }

    $uchwyt = fopen($sciezka, 'r');

    if ($uchwyt === false) {
        throw new Exception("Nie udało się otworzyć pliku z zadaniami.");
    }

    $zadania = [];

    try {
        fgetcsv($uchwyt, 0, ';', '"', '');   // pierwszy wiersz to nagłówek — pomijamy go
        $numerLinii = 1;

        while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
            $numerLinii++;

            if ($wiersz === [null]) {
                continue;   // pusta linia — niegroźna, pomijamy ją jak w Lekcji 11
            }

            sprawdzWierszZadania($wiersz, $numerLinii);

            $zadania[] = [
                'id' => (int) $wiersz[0],
                'tytul' => $wiersz[1],
                'opis' => $wiersz[2],
                'termin' => $wiersz[3],
                'wykonane' => $wiersz[4] === '1',
                'xp' => (int) $wiersz[5]
            ];
        }
    } finally {
        fclose($uchwyt);   // wykona się zawsze — także wtedy, gdy wyżej poleciał wyjątek
    }

    return $zadania;
}


function zapiszZadania(string $sciezka, array $zadania): void
{
    $uchwyt = fopen($sciezka, 'w');

    if ($uchwyt === false) {
        throw new Exception("Nie udało się zapisać pliku z zadaniami.");
    }

    fputcsv($uchwyt, ['id', 'tytul', 'opis', 'termin', 'wykonane', 'xp'], ';', '"', '');

    foreach ($zadania as $zadanie) {
        fputcsv($uchwyt, [
            $zadanie['id'],
            $zadanie['tytul'],
            $zadanie['opis'],
            $zadanie['termin'],
            $zadanie['wykonane'] ? 1 : 0,
            $zadanie['xp']
        ], ';', '"', '');
    }

    fclose($uchwyt);
}

index.php

<?php

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';


// --- dane użytkownika ---
$imieUzytkownika = "Kama";
$login = "Kama_2026";


// --- dane zadań (z pliku CSV) ---
$zadania = [];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek->getMessage();
}


// --- dane grywalizacji (symulowane) ---
$xp = 99;
$streak = 5;
$dataOstatniejAktywnosci = "2026-09-14";


// --- wywołania funkcji: grywalizacja ---
$poziom = obliczPoziom($xp);
$tytul = pobierzTytul($poziom);
$xpWAktualnymPoziomie = obliczXpWAktualnymPoziomie($xp, $poziom);
$procentPostepu = obliczProcentPostepu($xpWAktualnymPoziomie);
$brakujeXp = ileBrakujeXp($xp, $poziom);

//pasek postępu
$liczbaAktywnychSegmentow = (int) ($procentPostepu / 10);
$progNastepnegoPoziomu = $poziom * XP_NA_POZIOM;


// --- wywołania funkcji: lista zadań ---
$wykonaneZadania = pobierzWykonaneZadania($zadania);
$aktywneZadania = pobierzAktywneZadania($zadania);

$liczbaWszystkich = count($zadania);
$liczbaWykonanych = count($wykonaneZadania);
$liczbaAktywnych = count($aktywneZadania);

$xpDoZdobycia = sumujXp($aktywneZadania);

$zadaniaWgTerminu = sortujPoTerminie($zadania);
$aktywneWgTerminu = sortujPoTerminie($aktywneZadania);

// uproszczenie (symulacja): aktywność = wykonane jakiekolwiek zadanie z listy
$zadanieWykonane = $liczbaWykonanych > 0;

$streak = aktualizujStreak($streak, $dataOstatniejAktywnosci, $zadanieWykonane);


// --- prezentacja ---
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <p>Zdobywaj punkty za wykonane zadania!</p>

        <?php
        if (walidujLogin($login)) {
            echo "<p>Login poprawny: $login</p>";
        } else {
            echo "<p>Login jest niepoprawny.</p>";
        }
        ?>

        <h2>Witaj, <?= $imieUzytkownika; ?>!</h2>

        <p>Poziom: <strong><?= $poziom; ?></strong></p>
        <p>Tytuł: <strong><?= $tytul; ?></strong></p>
        <p>Łączne XP: <strong><?= $xp; ?></strong></p>

        <h3>
            <?= "Masz $xpWAktualnymPoziomie XP z " . XP_NA_POZIOM . " XP potrzebnych do poziomu " . ($poziom + 1) . " (" . round($procentPostepu, 2) . "%)"; ?>
        </h3>

        <div class="pasek-xp">
            <?php
            for ($segment = 1; $segment <= 10; $segment++) {
                if ($segment <= $liczbaAktywnychSegmentow) {
                    echo "█";
                } else {
                    echo "░";
                }
            }
            ?>
        </div>

        <p>Kolejny próg: <?= $progNastepnegoPoziomu; ?> XP</p>
        <p>Brakuje XP: <strong><?= $brakujeXp; ?></strong></p>
        <p>Streak: <strong><?= $streak ?></strong></p>

        <h2>Twoje zadania</h2>

        <?php if ($bladZadan !== null) { ?>
        <p class="blad">Nie udało się wczytać zadań: <?= $bladZadan; ?></p>
        <?php } else { ?>

        <p>
            Wszystkie: <strong><?= $liczbaWszystkich; ?></strong> |
            Wykonane: <strong><?= $liczbaWykonanych; ?></strong> |
            Aktywne: <strong><?= $liczbaAktywnych; ?></strong>
        </p>
        <p>XP do zdobycia z aktywnych zadań: <strong><?= $xpDoZdobycia; ?></strong></p>

        <?php
        if ($liczbaWszystkich === 0) {
            echo "<p>Brak zadań.</p>";
        } elseif ($liczbaAktywnych === 0) {
            echo "<p>Wszystkie zadania wykonane — świetna robota!</p>";
        } else {
            if ($xpDoZdobycia >= $brakujeXp) {
                echo "<p class=\"awans\">Wykonując aktywne zadania, awansujesz na poziom " . ($poziom + 1) . "!</p>";
            } else {
                echo "<p>Aktywne zadania nie wystarczą jeszcze na awans — brakuje " . ($brakujeXp - $xpDoZdobycia) . " XP.</p>";
            }

            $najpilniejsze = $aktywneWgTerminu[0];
            echo "<p>Najpilniejsze zadanie: <strong>" . wyczyscTytul($najpilniejsze['tytul']) . "</strong> — "
                . komunikatTerminu($najpilniejsze['termin']) . "</p>";
        }

        echo "<h3>Lista zadań (wg terminu)</h3>";

        foreach ($zadaniaWgTerminu as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie['tytul']);
            $znacznik = $zadanie['wykonane'] ? "[X]" : "[ ]";
            $klasa = $zadanie['wykonane'] ? "zadanie wykonane" : "zadanie";

            echo "<div class=\"$klasa\">";
            echo "<p><strong>$znacznik $tytulCzysty</strong> — {$zadanie['xp']} XP</p>";
            echo "<p>" . skrocOpis($zadanie['opis'], 50) . "</p>";
            echo "<p>Termin: {$zadanie['termin']}";

            if (!$zadanie['wykonane']) {
                echo " — " . komunikatTerminu($zadanie['termin']);
            }

            echo "</p>";

            if (stripos($tytulCzysty, 'PHP') !== false) {
                echo "<p>To zadanie dotyczy PHP.</p>";
            }

            echo "</div>";
        }
        ?>
        <?php } ?>
<?php
require_once __DIR__ . '/includes/footer.php';

includes/header.php

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $nazwaStrony; ?></title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 700px; margin: 40px auto; padding: 20px; }
        .panel { border: 1px solid #ccc; padding: 24px; border-radius: 12px; }
        .pasek-xp { display: flex; gap: 4px; margin: 10px 0; }
        .segment { width: 32px; height: 18px; background: #ddd; border-radius: 3px; }
        .segment.aktywny { background: #555; }
        .awans { font-weight: bold; }
        .zadanie { border: 1px solid #ddd; border-radius: 8px; padding: 4px 16px; margin: 10px 0; }
        .zadanie.wykonane { background: #f2f2f2; color: #666; }
        .blad { border: 1px solid #c00; background: #fdecec; color: #900; border-radius: 8px; padding: 8px 16px; }
    </style>
</head>
<body>
    <div class="panel">
        <h1>TaskQuest</h1>

includes/footer.php

        <p>TaskQuest 1.2</p>
    </div>
</body>
</html>

10. Co zmieniliśmy w TaskQuest?

Zostaje

$imieUzytkownika, $login
$xp, $poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp,
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu
$streak, $dataOstatniejAktywnosci, $zadanieWykonane
$zadania, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $xpDoZdobycia
$nazwaStrony
define('XP_NA_POZIOM', 100), define('PLIK_ZADAN', ...)

wszystkie funkcje z Lekcji 1–11 (kod bez zmian, poza dwiema funkcjami plikowymi — patrz niżej)
struktura tablicy $zadania (klucze: id, tytul, opis, termin, wykonane, xp)
format data/zadania.csv

Dochodzi

$bladZadan                       // index.php: null albo komunikat o awarii wczytania zadań
sprawdzWierszZadania()           // includes/funkcje_plikow.php: rzuca wyjątek, gdy wiersz CSV jest zły
.blad                            // klasa CSS w header.php

Nowe narzędzia PHP: throw new Exception(), try / catch / finally, getMessage() (oraz poznane: getCode(), getLine(), getFile(), getTraceAsString()), wyjątki lecące w górę przez funkcje, rozróżnienie Exception i Error.

Zmieniamy

  • wczytajZadania(): przy awarii pliku i przy uszkodzonym wierszu rzuca wyjątek (zamiast zwracać [] / po cichu pomijać wiersz); pustą linię nadal pomija; plik zamyka w finally,
  • zapiszZadania(): typ zwracany bool → void; gdy fopen() się nie uda, rzuca wyjątek (dotychczas zwracała false, którego nikt nie sprawdzał),
  • index.php: wczytanie zadań w try / catch; przy awarii sekcja zadań pokazuje ramkę z komunikatem,
  • includes/footer.php: TaskQuest 1.2.

Usuwamy

  • z wczytajZadania() instrukcję if (count($wiersz) !== 6) { continue; } — po cichu gubiła zadania; jej rolę przejęła sprawdzWierszZadania() (która zgłasza problem),
  • z zapiszZadania() instrukcje return false; i return true; — funkcja zgłasza awarię wyjątkiem.

Żadna zmienna ani funkcja z Lekcji 11 nie została usunięta.

Co nowego potrafi TaskQuest po tej lekcji?

Po Lekcji 12 TaskQuest odróżnia „brak zadań” od awarii. Gdy plik z zadaniami nie istnieje, nie da się go odczytać albo któryś wiersz jest uszkodzony, aplikacja nie udaje, że wszystko gra: pokazuje w ramce konkretny komunikat (z numerem linii), a reszta strony — poziom, XP, pasek i streak — działa dalej. Plik jest zawsze poprawnie zamykany, a zły termin, wykonane lub xp zostają zatrzymane przy wczytywaniu, zamiast wywracać stronę w innym miejscu. Zapis zadań też nie potrafi już „udać”, że się udał.

Do samodzielnego sprawdzenia

[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię wyjaśnić, co robią throw, try, catch i finally.
[ ] Wiem, że po throw reszta bloku try jest pomijana, a niezłapiony wyjątek kończy skrypt.
[ ] Rozumiem regułę: kto wykrywa problem — rzuca; kto wie, co z nim zrobić — łapie.
[ ] Wiem, kiedy NIE rzucać wyjątku (pusty plik, zły login, wszystko wykonane).
[ ] wczytajZadania() rzuca wyjątki: brak pliku, brak uprawnień, błąd fopen(), zły wiersz.
[ ] Mam sprawdzWierszZadania() z czterema sprawdzeniami (liczba pól, termin, wykonane, id/xp).
[ ] wczytajZadania() zamyka plik w bloku finally, a return $zadania; jest poza try.
[ ] zapiszZadania() ma typ void i rzuca wyjątek, gdy nie może otworzyć pliku.
[ ] index.php ma try/catch, $bladZadan i ramkę .blad; $zadania ma wartość początkową [].
[ ] Test 1 (13 przypadków) — przewidziałam/przewidziałem wyniki i wszystkie się zgadzają.
[ ] Test 3 daje bool(true), Test 4 — wszystkie sześć wierszy zgodne z tabelą.
[ ] Strona główna wygląda jak po Lekcji 11 (różnice: TaskQuest 1.2 i reguła .blad).
[ ] diagnostyka.php nadal pokazuje 11 razy [OK].
[ ] Usunęłam/usunąłem brudnopis.php i data/test.csv, przywróciłam/przywróciłem zadania.csv.
[ ] Przeprowadziłam/przeprowadziłem eksperymenty i przywróciłam/przywróciłem kod.

Pytania kontrolne


  1. Co robi throw, co try, a co catch? Co dzieje się z linią kodu znajdującą się w bloku try po linii, w której poleciał wyjątek?



  2. Przewidź, co wypisze poniższy kod (litera po literze), i uzasadnij kolejność:


    function f(int $n): int
    {
    if ($n < 0) {
    throw new Exception("ujemna");
    }
    return $n * 2;
    }

    try {
    echo "A ";
    echo f(3) . " ";
    echo f(-1) . " ";
    echo "B ";
    } catch (Exception $wyjatek) {
    echo "C ";
    } finally {
    echo "D ";
    }
    echo "E";


  3. Dlaczego wczytajZadania() nie łapie własnego wyjątku, tylko rzuca go dalej? Co wie index.php, czego nie wie wczytajZadania()?



  4. Plik z zadaniami jest pusty, a drugi plik w ogóle nie istnieje. Dlaczego tylko drugi przypadek rzuca wyjątek? Dlaczego walidujLogin() nadal zwraca bool, a nie rzuca wyjątku dla złego loginu?



  5. Co stanie się na stronie po zmianie nazwy data/zadania.csv, jeśli w index.php wpiszesz pusty catch (Exception $wyjatek) { }? Które zdanie z Lekcji 11 znów będzie prawdziwe?



  6. Czym różni się try wewnątrz pętli od try wokół całej pętli (Eksperyment 1)? W wczytajZadania() pierwszy uszkodzony wiersz przerywa wczytywanie całej listy. Jakie to ma zalety (dla wiarygodności danych), a jakie wady (dla użytkownika)? Jak inaczej dałoby się to rozwiązać?



  7. Po co w wczytajZadania() fclose() jest w finally, a nie po pętli? Dlaczego brak finally nie dałby widocznego efektu w naszej małej aplikacji, a mimo to jest złym nawykiem?



  8. Wywołujesz dodajXp(50, "abc") w bloku try z catch (Exception $wyjatek). Co się stanie i dlaczego? Czy to dobrze, że taki błąd zatrzymuje program?



  9. Termin 2026-13-45 w CSV przechodzi nasze sprawdzenie. Gdzie i w jaki sposób ta awaria się objawi? Jak można ją wyłapać przy wczytywaniu?



  10. Dlaczego użytkownikowi pokazujemy getMessage(), ale getFile() i getTraceAsString() — nie? W której lekcji wrócimy do tego tematu?



  11. Zmiana zapiszZadania() z bool na wyjątek jest w tej chwili bezpieczna. Dlaczego? Co musiałabyś/musiałbyś zrobić, gdyby ta funkcja była wywoływana w dwudziestu miejscach aplikacji?


Zadanie końcowe — rozbuduj TaskQuest samodzielnie

Wybierz jedno (albo oba):

A) Sprawdzanie prawdziwej daty. Termin 2026-02-31 albo 2026-13-45 ma poprawny kształt, ale nie istnieje w kalendarzu. Rozbuduj sprawdzWierszZadania() tak, żeby taki termin zgłaszał wyjątek Linia N: termin 'X' nie istnieje w kalendarzu. Do sprawdzenia użyj funkcji checkdate($miesiac, $dzien, $rok), która przyjmuje trzy liczby całkowite i zwraca true lub false. Trzy liczby z tekstu 2026-02-31 wydobędziesz np. przez explode('-', $wiersz[3]) i rzutowanie (int). Przetestuj: 2026-02-31 i 2026-13-45 mają być odrzucone, a 2026-02-28 przyjęte. Umieść nowe sprawdzenie po sprawdzeniu kształtu — dlaczego kolejność jest ważna?

B) Dziennik awarii. Napisz funkcję zapiszBladDoLogu(string $komunikat, string $plik, int $linia): void (w includes/funkcje_plikow.php), która dopisuje (nie kasuje!) do pliku logs/taskquest.log linię w formacie 2026-09-21 10:15:03 — Plik z zadaniami nie istnieje. (funkcje_plikow.php:29). Data: date('Y-m-d H:i:s'). Dopisywanie zrób przez file_put_contents() z flagą FILE_APPEND, a katalog logs/ utwórz, jeśli go nie ma (is_dir() + mkdir(), jak w Lekcji 10). Wywołaj funkcję w catch w index.php, przekazując $wyjatek->getMessage(), basename($wyjatek->getFile()) i $wyjatek->getLine(). Zadbaj o to, żeby zapisywanie logu nigdy nie zepsuło strony — jeśli zapis się nie uda, użytkownik nadal ma zobaczyć ramkę z komunikatem. Tu właśnie getFile() i getLine() są na swoim miejscu: to informacje dla programisty, nie dla odwiedzającego. Dopisz logs/taskquest.log do listy w diagnostyce tylko wtedy, gdy uznasz go za stałą część projektu.

Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory (poza checkdate() i explode() z podpowiedzi).

Most do Lekcji 13

TaskQuest potrafi już bezpiecznie wczytać zadania i głośno zgłosić problem. Ale dane wciąż wpisujemy ręcznie w edytorze. Kama chce dodać zadanie z poziomu strony:

formularz w przeglądarce   →  ???  →  PHP  →  zapiszZadania()  →  data/zadania.csv

Zaraz pojawią się pytania, na które ta lekcja już częściowo zna odpowiedź:

Użytkownik zostawi pusty tytuł          →  kto to sprawdzi i jak zgłosi?
W polu XP wpisze "dużo"                 →  jak zatrzymać złe dane przy wejściu?
zapiszZadania() nie zdoła otworzyć pliku →  co zobaczy użytkownik po kliknięciu "Dodaj"?

Wyjątki dają nam sposób zgłaszania i obsługi takich sytuacji. Brakuje jeszcze sposobu, w jaki PHP w ogóle dostaje dane z przeglądarki. To temat Lekcji 13: formularze HTML oraz metody GET i POST.


PODSUMOWANIE LEKCJI

W skrócie

  • Wyjątek to obiekt. new Exception('komunikat') sam z siebie nic nie robi — dopiero throw przerywa działanie kodu.
  • Po throw reszta bloku try jest pomijana. Sterowanie przeskakuje do pasującego catch.
  • Wyjątek leci w górę. Jeśli funkcja go nie łapie, przelatuje do funkcji wywołującej, i dalej — aż trafi na catch. Jak nie trafi: Fatal error: Uncaught Exception i koniec skryptu.
  • Reguła podziału ról: kto wykrywa problem — rzuca; kto wie, co z problemem zrobić — łapie.
  • catch dopasowuje po typie. catch (Exception $w) złapie Exception i każdą klasę po niej dziedziczącą. Przy kilku blokach catch kolejność idzie od najbardziej szczegółowego do najbardziej ogólnego — catch (Exception) postawiony jako pierwszy „zasłoni” wszystkie pozostałe.
  • Error to nie Exception. TypeError, ArgumentCountError i spółka to błędy w programie, których catch (Exception $w) nie łapie — i dobrze, bo mają zostać naprawione, a nie obsłużone.
  • finally wykona się zawsze — po udanym try, po złapanym wyjątku, nawet przy return. Służy do sprzątania (fclose(), zamknięcie połączenia). Nigdy nie wstawiaj do niego return — nadpisze wynik z try.
  • Wyjątek to awaria, nie zwykły if. Brak pliku, uszkodzone dane, brak uprawnień — tak. Pusta lista zadań, niepoprawny login w walidacji, wszystko wykonane — nie, to normalne stany aplikacji.
  • Pusty catch jest gorszy niż brak obsługi błędów — awaria znika bez śladu, a program udaje, że wszystko gra.
  • Użytkownikowi pokazujemy tylko getMessage(). getFile(), getLine() i getTraceAsString() zdradzają ścieżki na serwerze — to materiał do logu, nie na stronę.
  • Własny wyjątek to klasa dziedzicząca po Exception: class BladPliku extends Exception {}. Dzięki temu można łapać konkretny rodzaj awarii, a nie „cokolwiek”.

Co trzeba umieć na INF.03?

  • odczytać komunikat błędu PHP (Fatal error: Uncaught ..., nazwa pliku, numer linii) i na jego podstawie znaleźć miejsce awarii,
  • sprawdzić wynik operacji na pliku przed użyciem go dalej (fopen() zwracające false, brak pliku, brak uprawnień),
  • zareagować na awarię komunikatem dla użytkownika zamiast wyświetlać surowy błąd serwera,
  • rozróżniać błąd w kodzie (do naprawienia) od sytuacji wyjątkowej w danych (do obsłużenia),
  • przetestować skrypt na złych danych, nie tylko na poprawnych.

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. throw new Exception('komunikat'); — bez throw obiekt wyjątku nic nie robi.
  2. Schemat: try { kod } catch (Exception $wyjatek) { obsługa } finally { sprzątanie } — catch i finally są opcjonalne, ale co najmniej jedno z nich musi wystąpić.
  3. Po throw reszta bloku try się nie wykona. Niezłapany wyjątek kończy skrypt.
  4. Wyjątek przelatuje przez kolejne funkcje w górę, aż znajdzie catch.
  5. Kto wykrywa problem — rzuca. Kto wie, co z nim zrobić — łapie.
  6. $wyjatek->getMessage() — dla użytkownika. getFile(), getLine(), getTraceAsString() — do logu.
  7. finally wykona się zawsze; return w finally nadpisuje wynik z try.
  8. Wyjątek rzucamy przy awarii, nie przy normalnym stanie aplikacji.
  9. catch (Exception $w) nie złapie TypeError ani innego Error.
  10. Pusty catch = cichy błąd = najgorszy możliwy wariant.

Ściągawka – najważniejsze polecenia i składnia

Podstawowy schemat

try {
    // kod, który może się nie udać
} catch (Exception $wyjatek) {
    echo 'Błąd: ' . $wyjatek->getMessage();
} finally {
    // wykona się zawsze (opcjonalne)
}

Rzucanie

throw new Exception('Komunikat błędu');       // sam komunikat
throw new Exception('Komunikat błędu', 100);  // komunikat + kod

Metody obiektu wyjątku

MetodaZwracaDo czego
getMessage()stringkomunikat dla użytkownika
getCode()intkod błędu (domyślnie 0)
getFile()stringplik, w którym rzucono — do logu
getLine()intnumer linii — do logu
getTraceAsString()stringślad wywołań — do logu

Typowy wzorzec: plik + finally

$plik = fopen($sciezka, 'r');
if ($plik === false) {
    throw new Exception("Nie udało się otworzyć pliku $sciezka.");
}

try {
    // czytanie pliku
} finally {
    fclose($plik);   // zamknie się nawet po wyjątku
}

Własny wyjątek i kilka bloków catch

class BladPliku extends Exception {}
class BladDanych extends Exception {}

try {
    throw new BladDanych('Linia 4: zły format terminu.');
} catch (BladPliku $wyjatek) {      // najpierw szczegółowe
    echo 'Problem z plikiem: ' . $wyjatek->getMessage();
} catch (BladDanych $wyjatek) {
    echo 'Problem z danymi: ' . $wyjatek->getMessage();
} catch (Exception $wyjatek) {      // na końcu ogólne
    echo 'Inna awaria: ' . $wyjatek->getMessage();
}

Rzucać czy nie rzucać

SytuacjaWyjątek?
plik nie istnieje / brak uprawnieńtak
uszkodzony wiersz danychtak
nie udało się otworzyć pliku do zapisutak
pusta lista wynikównie
walidacja loginu / hasła daje falsenie
wszystkie zadania wykonanenie

Quiz – sprawdź się

1. Co wypisze ten kod?

function sprawdz(int $xp): void {
    if ($xp > 100) {
        throw new Exception('za dużo');
    }
    echo "ok($xp) ";
}

foreach ([50, 150, 70] as $xp) {
    try {
        sprawdz($xp);
    } catch (Exception $wyjatek) {
        echo 'blad ';
    }
}
echo 'koniec';

2. A co wypisze ten sam kod, jeśli try/catch otoczy całą pętlę foreach zamiast jej wnętrza? Które ustawienie wybierzesz, gdy chcesz przetworzyć jak najwięcej poprawnych danych?

3. Znajdź błąd — dlaczego ta funkcja nie zgłasza awarii?

function otworz(string $sciezka) {
    if (!file_exists($sciezka)) {
        new Exception("Plik $sciezka nie istnieje.");
    }
    return fopen($sciezka, 'r');
}

4. Co wypisze ten kod?

function licz(): int {
    $wynik = 0;
    try {
        $wynik = 1;
        return $wynik;
    } finally {
        $wynik = 9;
        echo 'sprzatam ';
    }
}

echo licz();

5. Czy poniższy catch przechwyci ten wyjątek? Uzasadnij.

class BladPliku extends Exception {}

try {
    throw new BladPliku('brak pliku');
} catch (Exception $wyjatek) {
    echo 'Złapano: ' . $wyjatek->getMessage();
}

6. Uzupełnij kod tak, aby funkcja zgłaszała awarię zamiast zapisywać do nieotwartego pliku:

function zapisz(string $sciezka, string $tekst): void {
    $plik = fopen($sciezka, 'w');
    if ($plik === false) {
        ________________________________________;
    }
    fwrite($plik, $tekst);
    fclose($plik);
}

7. Dla każdej sytuacji zdecyduj: rzucić wyjątek czy nie?

a) wyszukiwarka zadań nie znalazła ani jednego pasującego zadania
b) plik config/config.php nie istnieje
c) użytkownik wpisał hasło krótsze niż 8 znaków
d) katalog data/ nie ma prawa zapisu

8. Dlaczego pusty catch (Exception $wyjatek) { } jest gorszy niż całkowity brak try/catch?

9. Kod poniżej kończy się Fatal error, mimo że jest w try. Dlaczego catch nie zadziałał?

function dodajXp(int $xp, int $nagroda): int {
    return $xp + $nagroda;
}

try {
    echo dodajXp(50, 'abc');
} catch (Exception $wyjatek) {
    echo 'Złapano: ' . $wyjatek->getMessage();
}

10. Napisz funkcję wczytajLiczbe(string $tekst): int, która zwraca liczbę całkowitą, a dla tekstu, który nie jest liczbą, rzuca wyjątek z komunikatem "'abc' to nie jest liczba.". Pokaż też użycie w try/catch. (Przyda się is_numeric().)

11. W aplikacji działającej na prawdziwym serwerze, w bloku catch, które dane wypiszesz na stronie, a które zapiszesz do pliku z logiem? Wymień konkretne metody.


Odpowiedzi

1. ok(50) blad ok(70) koniec

try jest wewnątrz pętli, więc złapanie wyjątku kończy tylko jeden obieg — następny startuje normalnie.

2. ok(50) blad koniec — wyjątek przerywa całą pętlę, wartość 70 nie zostanie przetworzona. Gdy zależy nam na przetworzeniu jak największej liczby poprawnych danych, try umieszczamy wewnątrz pętli. Gdy jeden zły element oznacza, że całemu zestawowi danych nie można ufać (jak w wczytajZadania()) — na zewnątrz.

3. Brakuje słowa throw. new Exception(...) tworzy obiekt, przypisuje go w próżnię i program leci dalej — do fopen() na nieistniejącym pliku. Poprawnie: throw new Exception("Plik $sciezka nie istnieje.");

4. sprzatam 1

finally wykona się przed faktycznym wyjściem z funkcji (stąd sprzatam przed wynikiem), ale wartość zwracana została ustalona już przy return — późniejsza zmiana $wynik na 9 nic nie daje. Gdyby w finally był return 9;, funkcja zwróciłaby 9 — i dlatego return w finally się nie stosuje.

5. Tak, wypisze Złapano: brak pliku. BladPliku dziedziczy po Exception, a catch dopasowuje wyjątek po typie wraz z klasami pochodnymi. Uwaga: gdyby po tym bloku stał jeszcze catch (BladPliku $wyjatek), nigdy by się nie wykonał — bardziej ogólny catch musi być ostatni.

6. throw new Exception("Nie udało się otworzyć pliku $sciezka do zapisu.");

Typ zwracany void jest tu celowy: funkcja albo zapisze, albo zgłosi awarię — nie ma wariantu „po cichu się nie udało”.

7.
a) nie — brak wyników to normalna odpowiedź, wystarczy komunikat „Nic nie znaleziono.”;
b) tak — bez konfiguracji aplikacja nie ma jak działać;
c) nie — to walidacja, oczekiwany wynik to false plus komunikat przy formularzu;
d) tak — funkcja nie jest w stanie wykonać swojego zadania.

8. Bo awaria znika bez żadnego śladu: użytkownik widzi stronę, która wygląda poprawnie, ale pokazuje nieprawdziwe dane (np. „Brak zadań.” zamiast „Nie mogę odczytać pliku”). Fatal error przynajmniej jest widoczny i mówi, gdzie szukać. Cichy błąd potrafi być niezauważony tygodniami.

9. Bo to nie jest Exception, tylko TypeError — czyli Error. PHP odrzuca wywołanie dodajXp(50, 'abc') jeszcze przed wejściem do funkcji, ponieważ parametr jest zadeklarowany jako int. Error i Exception to dwie osobne rodziny (łączy je dopiero wspólny interfejs Throwable), a catch (Exception ...) łapie tylko tę drugą. I bardzo dobrze: to błąd w kodzie, który trzeba poprawić, a nie awaria do obsłużenia.

10. Przykładowe rozwiązanie:

function wczytajLiczbe(string $tekst): int {
    if (!is_numeric($tekst)) {
        throw new Exception("'$tekst' to nie jest liczba.");
    }
    return (int) $tekst;
}

try {
    echo wczytajLiczbe('42');    // 42
    echo wczytajLiczbe('abc');   // rzuca wyjątek
    echo 'ta linia się nie wykona';
} catch (Exception $wyjatek) {
    echo 'Błąd: ' . $wyjatek->getMessage();
}

11. Na stronie: tylko $wyjatek->getMessage() — i to pod warunkiem, że komunikat jest napisany z myślą o użytkowniku. Do logu: getMessage(), getFile() (najlepiej skrócone przez basename()), getLine() oraz getTraceAsString(), czyli dane potrzebne programiście do odnalezienia miejsca awarii. Pokazanie śladu wywołań na stronie ujawnia strukturę katalogów serwera.