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.
W trakcie wykonywania skryptów, często okazuje się, że natrafiamy na błędy. W takiej sytuacji skrypt kończy swoje działanie, a istnieją sytuacje, w których nie jest to konieczne np. praca na nieistniejącym pliku.
I tutaj z pomocą przychodzą nam wyjątki(mechanizm obsługi błędów), które wyświetlają stosowną informację dla użytkownika w przypadku błędu i umożliwiają kontynuację pracy skryptu.
Mówimy, że wyjątki przechwytują błędy.
Wyobraźmy sobie funkcję zwracającą odwrotność podanej liczby (1/x). Co się stanie, gdy ktoś przekaże 0?
Jak zabezpieczyć taki kod?
Przykład z nieobsłużonym błędem:
<?php
function odwrotnosc($x) {
if (!$x) {
return "Błąd: dzielenie przez 0"; // Zwracamy string
}
return 1 / $x; // Zwracamy liczbę
}
// Użycie - PROBLEMATYCZNE!
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
// Wynik: Odwrotność 5 to 0.2
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>';
// Wynik: Odwrotność 0 to Błąd: dzielenie przez 0
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>';
// Wynik: Odwrotność 7 to 0.142857...
echo 'Skrypt kontynuuje swoją pracę';
A teraz rozwiązanie z obsługą błędów:
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0'); // Rzuć wyjątek
}
return 1 / $x;
}
// ============================================
// Przykład BEZ przechwycenia błędu - ZATRZYMA SKRYPT
// ============================================
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>'; // FATAL ERROR!
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>'; // Nie wykona się
echo 'Skrypt kontynuuje swoją pracę'; // Nie wykona się
Wynik w przeglądarce:
Odwrotność 5 to 0.2
Fatal error: Uncaught Exception: Dzielenie przez 0 in /path/to/file.php:4
Stack trace:
#0 /path/to/file.php(20): odwrotnosc(0)
#1 {main}
thrown in /path/to/file.php on line 4
Skrypt się zatrzymał! To zachowanie domyślne – jeśli wyjątek nie zostanie przechwycony, PHP przerywa działanie.
Z przechwyceniem wyjątku – POPRAWNIE
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0');
}
return 1 / $x;
}
// Przykład z przechwyconym błędem
try {
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>'; // Wyjątek!
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>'; // Nie wykona się
} catch (Exception $e) {
echo 'Przechwycony wyjątek: ' . $e->getMessage() . '<br>';
}
echo 'Skrypt kontynuuje swoją pracę';
Wynik:
Odwrotność 5 to 0.2
Przechwycony wyjątek: Dzielenie przez 0
Skrypt kontynuuje swoją pracę
Podstawowa składnia
Kluczowe słowa: throw, try, catch
Wyjątki są obiektami klasy Exception, które posiadają własny komunikat i kod błędu. Mogą one zostać wyłapane (try), rzucone (throw) i przechwycone (catch).
<?php
// throw - rzuć wyjątek
throw new Exception('Komunikat błędu');
// try - spróbuj wykonać kod
try {
// Kod, który może rzucić wyjątek
}
// catch - przechwyć wyjątek
catch (Exception $e) {
// Obsłuż błąd
}
throw – rzucanie wyjątku
<?php
function podziel($a, $b) {
if ($b == 0) {
throw new Exception('Nie można dzielić przez zero');
}
return $a / $b;
}
throw – „rzuć wyjątek”, przerwij normalne działanie i przekaż kontrolę do catch
try – próba wykonania kodu
<?php
try {
// Kod, który może generować błędy
echo podziel(10, 2); // OK
echo podziel(10, 0); // Wyjątek!
echo podziel(10, 5); // Nie wykona się
}
try – „spróbuj wykonać ten kod, ale przygotuj się na wyjątki”
catch – przechwycenie wyjątku
<?php
try {
echo podziel(10, 0);
} catch (Exception $e) {
echo 'Błąd: ' . $e->getMessage();
}
catch – „jeśli w try wystąpi wyjątek, wykonaj ten kod”
Schemat działania
┌─────────────────────────────────────────┐
│ TRY (spróbuj) │
│ ┌─────────────────────────────────┐ │
│ │ echo odwrotnosc(5); │ │
│ │ echo odwrotnosc(0); ───► throw │ │
│ │ echo odwrotnosc(7); │ │
│ └─────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ CATCH (przechwyć) │
│ ┌─────────────────────────────────┐ │
│ │ echo "Błąd: " . $e->getMessage()│ │
│ └─────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
│
▼
Kontynuacja skryptu
MOŻLIWE METODY WYJĄTKÓW
Wyjątki są obiektami klasy Exception, które posiadają przydatne metody diagnostyczne.
Wszystkie metody
<?php
try {
odwrotnosc(0);
} catch (Exception $e) {
echo "INFORMACJE O WYJĄTKU:\n\n";
// 1. Wiadomość błędu
echo "getMessage(): " . $e->getMessage() . "\n";
// Dzielenie przez 0
// 2. Kod błędu (domyślnie 0, można ustawić własny)
echo "getCode(): " . $e->getCode() . "\n";
// 0
// 3. Plik, w którym wystąpił błąd
echo "getFile(): " . $e->getFile() . "\n";
// /var/www/html/test.php
// 4. Linia w pliku
echo "getLine(): " . $e->getLine() . "\n";
// 4
// 5. Ślad wywołań (array) - backtrace
echo "getTrace():\n";
print_r($e->getTrace());
// 6. Ślad wywołań jako string (czytelniejszy)
echo "getTraceAsString():\n";
echo $e->getTraceAsString() . "\n";
// 7. Poprzedni wyjątek (jeśli istnieje)
echo "getPrevious(): ";
var_dump($e->getPrevious());
// NULL (brak poprzedniego)
}
Tabela metod Exception
| Metoda | Zwraca | Przykład | Zastosowanie |
|---|---|---|---|
getMessage() | string | „Dzielenie przez 0” | Komunikat dla użytkownika |
getCode() | int | 0 lub własny kod | Kategoryzacja błędów |
getFile() | string | „test.php” | Debugowanie |
getLine() | int | 4 | Debugowanie |
getTrace() | array | […] | Zaawansowane debugowanie |
getTraceAsString() | string | „#0 test.php(10)…” | Logowanie błędów |
getPrevious() | Exception|null | null | Łańcuch błędów |
Przykład praktyczny z wszystkimi metodami
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0', 100); // z kodem błędu
}
return 1 / $x;
}
try {
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>';
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '<br>';
} catch (Exception $e) {
echo "<pre>";
echo "Przechwycony wyjątek:\n";
echo "• Wiadomość: " . $e->getMessage() . "\n";
echo "• Kod: " . $e->getCode() . "\n";
echo "• Plik: " . basename($e->getFile()) . "\n";
echo "• Linia: " . $e->getLine() . "\n";
echo "\nŚlad wywołań:\n" . $e->getTraceAsString();
echo "</pre>";
}
echo 'Skrypt kontynuuje swoją pracę';
Wynik:
Odwrotność 5 to 0.2
Przechwycony wyjątek:
• Wiadomość: Dzielenie przez 0
• Kod: 100
• Plik: test.php
• Linia: 4
Ślad wywołań:
#0 test.php(12): odwrotnosc(0)
#1 {main}
Skrypt kontynuuje swoją pracę
FINALLY
Finally służy do określenia kodu, który będzie wykonywany zawsze, niezależnie od wykrycia wyjątku.
<?php
function odwrotnosc($x) {
if (!$x) {
throw new Exception('Dzielenie przez 0');
}
return 1 / $x;
}
// Przykład 1: BEZ wyjątku
try {
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '<br>';
} catch (Exception $e) {
echo 'Błąd: ' . $e->getMessage() . '<br>';
} finally {
echo 'Blok finally wykonany (BEZ wyjątku)<br><br>';
}
// Przykład 2: Z wyjątkiem
try {
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '<br>';
} catch (Exception $e) {
echo 'Błąd: ' . $e->getMessage() . '<br>';
} finally {
echo 'Blok finally wykonany (Z wyjątkiem)<br><br>';
}
echo 'Skrypt kontynuuje działanie';
Wynik:
Odwrotność 5 to 0.2
Blok finally wykonany (BEZ wyjątku)
Błąd: Dzielenie przez 0
Blok finally wykonany (Z wyjątkiem)
Skrypt kontynuuje działanie
Należy jednak uważać podczas używania finally z return!
Jeżeli w bloku try… catch… zostanie wykryta instrukcja return, blok finally i tak zostanie wykonany. Dodatkowo, jeżeli blok finally też zawiera return, to zwrócona zostanie zawartość finally.
Problem: return w finally nadpisuje return z try lub catch!
<?php
//problematyczny kod
function test() {
try {
echo "try<br>";
return 1;
} catch (Exception $e) {
echo "catch<br>";
return 2;
} finally {
echo "finally<br>";
return 3; // TO NADPISZE poprzedni return!
}
return 4; // Nigdy się nie wykona
}
$wynik = test();
echo "Wynik: $wynik";
//zostanie wyświetlone
//try
//finally
//Wynik: 3
//poprawne rozwiązanie
function test() {
$result = null;
try {
$result = 1;
} catch (Exception $e) {
$result = 2;
} finally {
// Tylko cleanup, bez return
echo "Cleanup wykonany<br>";
}
return $result;
}
Praktyczne zastosowania finally
1. Zamykanie pliku
<?php
$plik = null;
try {
$plik = fopen('dane.txt', 'r');
if ($plik === false) {
throw new Exception("Nie można otworzyć pliku");
}
// Operacje na pliku...
$zawartosc = fread($plik, filesize('dane.txt'));
echo $zawartosc;
} catch (Exception $e) {
echo "Błąd: " . $e->getMessage();
} finally {
// ZAWSZE zamknij plik
if ($plik !== null) {
fclose($plik);
echo "Plik zamknięty<br>";
}
}
2. Zamykanie połączenia z bazą danych
<?php
$db = null;
try {
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
// Operacje na bazie...
$stmt = $db->query("SELECT * FROM users");
$users = $stmt->fetchAll();
} catch (PDOException $e) {
echo "Błąd bazy: " . $e->getMessage();
} finally {
// ZAWSZE zamknij połączenie
$db = null;
echo "Połączenie zamknięte<br>";
}
3. Zwalnianie zasobów
<?php
function processLargeFile($filename) {
$resource = acquireResource(); // Zdobądź zasób
try {
// Przetwarzanie...
if (!file_exists($filename)) {
throw new Exception("Plik nie istnieje");
}
// Długa operacja...
processFile($filename, $resource);
} catch (Exception $e) {
echo "Błąd: " . $e->getMessage();
} finally {
// ZAWSZE zwolnij zasób
releaseResource($resource);
echo "Zasób zwolniony<br>";
}
}
TWORZENIE WŁASNYCH WYJĄTKÓW
Wyjątki możemy tworzyć wg własnego uznania. Wystarczy, że stworzymy klasę dziedziczącą po klasie Exception.
<?php
class MojWyjatek extends Exception {
public function __construct() {
parent::__construct("To jest mój autorski wyjątek");
}
}
function testMojWyjatek() {
throw new MojWyjatek();
}
try {
testMojWyjatek();
} catch (MojWyjatek $e) {
echo "Błąd: " . $e->getMessage();
}
// Wynik: Błąd: To jest mój autorski wyjątek
Własny wyjątek z dodatkowymi danymi
<?php
class DivisionByZeroException extends Exception {
private $dividend;
public function __construct($dividend) {
$this->dividend = $dividend;
parent::__construct("Nie można podzielić $dividend przez 0");
}
public function getDividend() {
return $this->dividend;
}
}
function odwrotnosc($x) {
if (!$x) {
throw new DivisionByZeroException($x);
}
return 1 / $x;
}
try {
echo odwrotnosc(0);
} catch (DivisionByZeroException $e) {
echo "Błąd: " . $e->getMessage() . "<br>";
echo "Próbowano podzielić: " . $e->getDividend();
}
Hierarchia własnych wyjątków
<?php
// Bazowy wyjątek aplikacji
class AppException extends Exception {}
// Wyjątki matematyczne
class MathException extends AppException {}
class DivisionByZeroException extends MathException {}
class NegativeNumberException extends MathException {}
// Wyjątki plików
class FileException extends AppException {}
class FileNotFoundException extends FileException {}
class FileNotReadableException extends FileException {}
// ============================================
// UŻYCIE
// ============================================
function odwrotnosc($x) {
if (!$x) {
throw new DivisionByZeroException('Dzielenie przez 0');
}
if ($x < 0) {
throw new NegativeNumberException('Liczba ujemna: ' . $x);
}
return 1 / $x;
}
try {
echo odwrotnosc(-5);
} catch (DivisionByZeroException $e) {
echo "Dzielenie przez zero!";
} catch (NegativeNumberException $e) {
echo "Liczba ujemna: " . $e->getMessage();
} catch (MathException $e) {
echo "Błąd matematyczny: " . $e->getMessage();
} catch (AppException $e) {
echo "Błąd aplikacji: " . $e->getMessage();
}
Przykład praktyczny – walidacja formularza
<?php
class ValidationException extends Exception {
private array $errors;
public function __construct(array $errors, string $message = "Błędy walidacji") {
parent::__construct($message);
$this->errors = $errors;
}
public function getErrors(): array {
return $this->errors;
}
}
function validateUser(array $data): void {
$errors = [];
if (empty($data['email'])) {
$errors[] = 'Email jest wymagany';
}
if (empty($data['password'])) {
$errors[] = 'Hasło jest wymagane';
}
if (strlen($data['password'] ?? '') < 8) {
$errors[] = 'Hasło musi mieć min. 8 znaków';
}
if (!empty($errors)) {
throw new ValidationException($errors);
}
}
// Użycie
try {
validateUser(['email' => '', 'password' => 'short']);
} catch (ValidationException $e) {
echo "Błędy walidacji:<br>";
foreach ($e->getErrors() as $error) {
echo "• $error<br>";
}
}
//Wynik:
//Błędy walidacji:
//• Email jest wymagany
//• Hasło musi mieć min. 8 znaków
Przykłady praktyczne
1. System logowania błędów
<?php
class ErrorLogger {
private string $logFile = 'errors.log';
public function log(Exception $e): void {
$timestamp = date('Y-m-d H:i:s');
$logEntry = sprintf(
"[%s] %s: %s\nPlik: %s:%d\nŚlad:\n%s\n%s\n",
$timestamp,
get_class($e),
$e->getMessage(),
basename($e->getFile()),
$e->getLine(),
$e->getTraceAsString(),
str_repeat('-', 80)
);
file_put_contents($this->logFile, $logEntry, FILE_APPEND);
}
}
// Użycie
$logger = new ErrorLogger();
try {
odwrotnosc(0);
} catch (Exception $e) {
$logger->log($e);
echo "Wystąpił błąd. Szczegóły w logu.";
}
2. Kalkulator z obsługą błędów
<?php
class CalculatorException extends Exception {}
class DivisionByZeroException extends CalculatorException {}
class InvalidOperationException extends CalculatorException {}
class Calculator {
public function calculate(float $a, string $op, float $b): float {
switch ($op) {
case '+':
return $a + $b;
case '-':
return $a - $b;
case '*':
return $a * $b;
case '/':
if ($b == 0) {
throw new DivisionByZeroException("Nie można dzielić przez 0");
}
return $a / $b;
default:
throw new InvalidOperationException("Nieznana operacja: $op");
}
}
}
// Użycie
$calc = new Calculator();
try {
echo $calc->calculate(10, '/', 0);
} catch (DivisionByZeroException $e) {
echo "Błąd dzielenia: " . $e->getMessage();
} catch (CalculatorException $e) {
echo "Błąd kalkulatora: " . $e->getMessage();
}
3. Upload plików z walidacją
<?php
class FileUploadException extends Exception {}
class FileUploader {
private string $uploadDir = 'uploads';
private array $allowedExt = ['jpg', 'png', 'pdf'];
private int $maxSize = 5242880; // 5MB
public function upload(array $file): string {
// Sprawdź błędy uploadu
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new FileUploadException($this->getErrorMessage($file['error']));
}
// Sprawdź rozmiar
if ($file['size'] > $this->maxSize) {
throw new FileUploadException("Plik za duży (max 5MB)");
}
// Sprawdź rozszerzenie
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $this->allowedExt)) {
throw new FileUploadException("Niedozwolone rozszerzenie: $ext");
}
// Zapisz
$filename = uniqid() . '.' . $ext;
$path = $this->uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $path)) {
throw new FileUploadException("Nie można zapisać pliku");
}
return $path;
}
private function getErrorMessage(int $code): string {
$errors = [
UPLOAD_ERR_INI_SIZE => 'Plik za duży',
UPLOAD_ERR_PARTIAL => 'Plik wysłany częściowo',
UPLOAD_ERR_NO_FILE => 'Nie wybrano pliku',
];
return $errors[$code] ?? 'Nieznany błąd';
}
}
// Użycie
if ($_FILES['file'] ?? false) {
$uploader = new FileUploader();
try {
$path = $uploader->upload($_FILES['file']);
echo "Plik przesłany: $path";
} catch (FileUploadException $e) {
echo "Błąd: " . $e->getMessage();
}
}
Zadanie 1:
Napisz funkcję dlugoscCiagu(), która:
- Przyjmuje string (lub null)
- Zwraca długość ciągu
- Rzuca wyjątek jeśli pusty lub null
Zadanie 2
Stwórz klasę BankAccount z metodami withdraw() i deposit() oraz obsługą wyjątków.
Zadanie 3
Napisz funkcję readConfig() odczytującą plik JSON z obsługą błędów.
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/catchi odczytywać komunikat przezgetMessage(), - 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 jestnew, 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 windex.php— to nadal symulacja.
Rozgrzewka — 4 pytania z Lekcji 11
Odpowiedz z pamięci, zanim zaczniesz:
- Który tryb
fopen()od razu kasuje zawartość pliku i dlaczego używamy go wzapiszZadania()? - Co zwraca
fgetcsv()dla pustej linii w pliku i jakwczytajZadania()radziła sobie z tym w Lekcji 11? - Po odczycie z pliku
wykonaneto tekst"1"albo"0". Jak zamieniamy go na wartość logiczną? - 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
| Plik | Co się zmienia |
|---|---|
includes/funkcje_plikow.php | wczytajZadania() i zapiszZadania() rzucają wyjątki; nowa sprawdzWierszZadania() |
index.php | try / catch wokół wczytania zadań, nowa zmienna $bladZadan, komunikat o błędzie |
includes/header.php | klasa .blad do ramki z komunikatem |
includes/footer.php | numer 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)
- Zrób kopię zapasową całego katalogu
taskquest/z Lekcji 11 — np. jakotaskquest_lekcja11/(obok, nie w środku). Przyda się do testu regresji. - Sprawdź, czy
data/zadania.csvma nagłówek i 5 zadań, aindex.phpidiagnostyka.phpdziałają (11 ×[OK]). - Zrób kopię pliku
data/zadania.csvpoza katalogiemtaskquest/(np. na pulpicie, jakozadania_kopia.csv). Będziemy go psuć w testach i eksperymentach, a chcemy mieć do czego wrócić. Kopii nie trzymaj wdata/— sekcja „Pliki danych” wdiagnostyka.phppokazał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.
| Sytuacja | Co widzi użytkownik dziś | Co powinno się stać |
|---|---|---|
| Plik nie istnieje | „Brak zadań.” | komunikat, że pliku nie ma |
| Wiersz jest uszkodzony | zadanie znika po cichu | komunikat, w której linii jest problem |
| Plik jest pusty (tylko nagłówek) | „Brak zadań.” | „Brak zadań.” — tu wszystko gra |
zapiszZadania() nie może otworzyć pliku | zwraca false, nikt tego nie sprawdza | zapis 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:
- sposobu na zgłoszenie awarii z dowolnego miejsca w kodzie,
- sposobu na przerwanie tego, co program właśnie robi,
- 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
$xpi liczyć dalej, - wołający może go po prostu zignorować (tak jak ignorujemy
falsezzapiszZadania()), - 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:
thrownatychmiast przerywa funkcję. Liniaecho "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 tracemó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
trynic się nie stanie, blokcatchjest po prostu pomijany. - Gdy wyjątek poleci, reszta bloku
tryjest pomijana — nawet jeśli była poprawna. $wyjatekto 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”:
| Metoda | Zwraca | Dla kogo |
|---|---|---|
getMessage() | tekst komunikatu | uż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 throw | programista |
getFile() | pełną ścieżkę pliku, w którym zrobiono throw | programista |
getTraceAsString() | „drogę” wywołań funkcji jako tekst | programista |
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
returndo blokufinally. Takireturnnadpisuje wynik zwrócony wtry(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 TaskQuest | Wyjątek? | Dlaczego |
|---|---|---|
| Plik z zadaniami jest pusty (tylko nagłówek) | nie | to normalny stan: „Brak zadań.” jest poprawną odpowiedzią |
| Plik z zadaniami nie istnieje | tak | funkcja nie może wykonać zadania |
| Wiersz CSV ma 5 pól zamiast 6 | tak | dane są uszkodzone, nie da się ich zinterpretować |
Login nie przechodzi regexa w walidujLogin() | nie | true / false to oczekiwana odpowiedź, nie awaria |
| Wszystkie zadania są wykonane | nie | normalny 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. tryopakowują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. returnwfinally— nadpisuje wynik ztry.- Zapomniany
throw— samnew Exception(...)bezthrownic 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:
- definiuje
dodajXp(int $xp, int $nagroda): int— dla ujemnej nagrody rzuca wyjątek z komunikatemNagroda XP nie może być ujemna., w przeciwnym razie zwraca sumę, - ustawia
$xp = 50i dla każdej nagrody z listy[20, 0, -5, 15](pętlaforeach) próbuje wywołaćdodajXp(), - wypisuje dla każdej nagrody jedną linię: albo
Nagroda 20 XP: razem 70, alboNagroda -5 XP: odrzucona — <komunikat wyjątku>, - 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:
| Przypadek | Komunikat (dokładnie taki) |
|---|---|
!is_file($sciezka) | Plik z zadaniami nie istnieje. |
!is_readable($sciezka) | Brak uprawnień do odczytu pliku z zadaniami. |
$uchwyt === false | Nie 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():
- usuń dotychczasowy fragment
if (count($wiersz) !== 6) { continue; }— to on po cichu gubił zadania, - numeruj linie pliku: nagłówek to linia
1, więc pierwsze zadanie jest w linii2(zmienna$numerLinii), - pustą linię (
fgetcsv()zwraca wtedy tablicę[null]) nadal pomijaj przezcontinue— to niegroźne i nie jest awarią, - dla pozostałych wierszy wywołaj
sprawdzWierszZadania(), - cały odczyt (nagłówek + pętla
while) umieść wtry { ... } 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:
- 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 wtry/catch. - 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. - W
includes/header.phpdodaj do<style>regułę dla klasy.blad. - W
includes/footer.phpzmieńTaskQuest 1.1naTaskQuest 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
$zadaniajest puste, więc$zadanieWykonane(uproszczenie z Lekcji 9) wychodzifalsei 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():
- typ zwracany z
boolnavoid, - gdy
fopen()zwrócifalse— rzuć wyjątekNie udało się zapisać pliku z zadaniami., - usuń
return true;z końca funkcji (funkcjavoidniczego 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 awarii | Komunikat (dokładnie taki) |
|---|---|
termin nie ma kształtu RRRR-MM-DD | Linia N: termin 'X' nie ma formatu RRRR-MM-DD. |
wykonane to nie 0 i nie 1 | Linia 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-45ma poprawny kształt, więc przejdzie — i dopieroDateTimewkomunikatTerminu()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ść pliku | Oczekiwany wynik |
|---|---|
| pusty plik (0 bajtów) | OK: 0 zadań |
| tylko nagłówek | OK: 0 zadań |
| nagłówek + 1 poprawny wiersz | OK: 1 zadań |
| nagłówek + wiersz + pusta linia + wiersz | OK: 2 zadań |
| wiersz z 5 polami | BŁĄD: Linia 2: oczekiwano 6 pól, a jest 5. |
| plik rozdzielony przecinkami | BŁĄD: Linia 2: oczekiwano 6 pól, a jest 1. |
termin jutro | BŁĄD: Linia 2: termin 'jutro' nie ma formatu RRRR-MM-DD. |
wykonane równe 2 | BŁĄD: Linia 2: pole wykonane musi mieć wartość 0 albo 1. |
xp równe abc | BŁĄD: Linia 2: id i xp muszą być liczbami całkowitymi. |
termin 2026-13-45 | OK: 1 zadań (!) — kształt jest poprawny; to ograniczenie z sekcji 6 |
| dwa poprawne wiersze i trzeci z 4 polami | BŁĄD: Linia 4: oczekiwano 6 pól, a jest 4. |
| linia zawierająca same spacje | BŁĄD: Linia 2: oczekiwano 6 pól, a jest 1. (to nie jest pusta linia!) |
| plik nie istnieje | BŁĄ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.
| Zmiana | Oczekiwany efekt na stronie |
|---|---|
plik nazywa się zadania_x.csv | poziom, 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 jutro | ramka: ... Linia 5: termin 'jutro' nie ma formatu RRRR-MM-DD. |
| w pliku tylko nagłówek | brak ramki, komunikat Brak zadań. (to nie awaria) |
pusta linia między zadaniami 2 i 3 | strona wygląda normalnie, nadal 5 zadań |
| oryginalny plik | strona 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:
- usuń
brudnopis.phporaz ewentualne pozostałościdata/test.csv, - upewnij się, że
data/zadania.csvma oryginalną zawartość (5 zadań) i oryginalną nazwę, - odśwież
index.phpidiagnostyka.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 wfinally,zapiszZadania(): typ zwracanybool→void; gdyfopen()się nie uda, rzuca wyjątek (dotychczas zwracałafalse, którego nikt nie sprawdzał),index.php: wczytanie zadań wtry/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ęłasprawdzWierszZadania()(która zgłasza problem), - z
zapiszZadania()instrukcjereturn false;ireturn 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,
wykonanelubxpzostają 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
Co robi
throw, cotry, a cocatch? Co dzieje się z linią kodu znajdującą się w blokutrypo linii, w której poleciał wyjątek?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";Dlaczego
wczytajZadania()nie łapie własnego wyjątku, tylko rzuca go dalej? Co wieindex.php, czego nie wiewczytajZadania()?Plik z zadaniami jest pusty, a drugi plik w ogóle nie istnieje. Dlaczego tylko drugi przypadek rzuca wyjątek? Dlaczego
walidujLogin()nadal zwracabool, a nie rzuca wyjątku dla złego loginu?Co stanie się na stronie po zmianie nazwy
data/zadania.csv, jeśli windex.phpwpiszesz pustycatch (Exception $wyjatek) { }? Które zdanie z Lekcji 11 znów będzie prawdziwe?Czym różni się
trywewnątrz pętli odtrywokół całej pętli (Eksperyment 1)? WwczytajZadania()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ć?Po co w
wczytajZadania()fclose()jest wfinally, a nie po pętli? Dlaczego brakfinallynie dałby widocznego efektu w naszej małej aplikacji, a mimo to jest złym nawykiem?Wywołujesz
dodajXp(50, "abc")w blokutryzcatch (Exception $wyjatek). Co się stanie i dlaczego? Czy to dobrze, że taki błąd zatrzymuje program?Termin
2026-13-45w CSV przechodzi nasze sprawdzenie. Gdzie i w jaki sposób ta awaria się objawi? Jak można ją wyłapać przy wczytywaniu?Dlaczego użytkownikowi pokazujemy
getMessage(), alegetFile()igetTraceAsString()— nie? W której lekcji wrócimy do tego tematu?Zmiana
zapiszZadania()zboolna 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 — dopierothrowprzerywa działanie kodu. - Po
throwreszta blokutryjest pomijana. Sterowanie przeskakuje do pasującegocatch. - 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 Exceptioni koniec skryptu. - Reguła podziału ról: kto wykrywa problem — rzuca; kto wie, co z problemem zrobić — łapie.
catchdopasowuje po typie.catch (Exception $w)złapieExceptioni każdą klasę po niej dziedziczącą. Przy kilku blokachcatchkolejność idzie od najbardziej szczegółowego do najbardziej ogólnego —catch (Exception)postawiony jako pierwszy „zasłoni” wszystkie pozostałe.Errorto nieException.TypeError,ArgumentCountErrori spółka to błędy w programie, którychcatch (Exception $w)nie łapie — i dobrze, bo mają zostać naprawione, a nie obsłużone.finallywykona się zawsze — po udanymtry, po złapanym wyjątku, nawet przyreturn. Służy do sprzątania (fclose(), zamknięcie połączenia). Nigdy nie wstawiaj do niegoreturn— nadpisze wynik ztry.- 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
catchjest 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()igetTraceAsString()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ącefalse, 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:
throw new Exception('komunikat');— bezthrowobiekt wyjątku nic nie robi.- Schemat:
try { kod } catch (Exception $wyjatek) { obsługa } finally { sprzątanie }—catchifinallysą opcjonalne, ale co najmniej jedno z nich musi wystąpić. - Po
throwreszta blokutrysię nie wykona. Niezłapany wyjątek kończy skrypt. - Wyjątek przelatuje przez kolejne funkcje w górę, aż znajdzie
catch. - Kto wykrywa problem — rzuca. Kto wie, co z nim zrobić — łapie.
$wyjatek->getMessage()— dla użytkownika.getFile(),getLine(),getTraceAsString()— do logu.finallywykona się zawsze;returnwfinallynadpisuje wynik ztry.- Wyjątek rzucamy przy awarii, nie przy normalnym stanie aplikacji.
catch (Exception $w)nie złapieTypeErrorani innegoError.- 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
| Metoda | Zwraca | Do czego |
|---|---|---|
getMessage() | string | komunikat dla użytkownika |
getCode() | int | kod błędu (domyślnie 0) |
getFile() | string | plik, w którym rzucono — do logu |
getLine() | int | numer 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ć
| Sytuacja | Wyjątek? |
|---|---|
| plik nie istnieje / brak uprawnień | tak |
| uszkodzony wiersz danych | tak |
| nie udało się otworzyć pliku do zapisu | tak |
| pusta lista wyników | nie |
walidacja loginu / hasła daje false | nie |
| wszystkie zadania wykonane | nie |
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.