13. PHP – odbieranie danych z przeglądarki – formularze html, metody GET i POST. Wysyłanie danych do przeglądarki.


Szybki quiz – przypomnienie poprzedniej lekcji

Lekcja 12: wyjątki (throw, try, catch, finally). Odpowiedz z pamięci, bez zaglądania do kodu.

1. Co zostanie wypisane?

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

try {
    echo "A";
    echo sprawdzNagrode(-3);
    echo "B";
} catch (Exception $wyjatek) {
    echo "C";
}
echo "D";

2. Znajdź błąd. Dlaczego ten kod nie zatrzymuje zapisu ujemnej nagrody?

if ($nagroda < 0) {
    new Exception("Nagroda nie może być ujemna.");
}
$xp = $xp + $nagroda;

3. Dla każdej sytuacji odpowiedz: wyjątek czy zwykła wartość zwrócona z funkcji?

a) pliku data/zadania.csv nie ma na dysku,
b) plik zawiera tylko wiersz nagłówka,
c) wiersz CSV ma 5 pól zamiast 6,
d) login nie przechodzi walidacji w walidujLogin().

4. W wczytajZadania() fclose() znalazło się w bloku finally, a nie za pętlą czytającą plik. Co by się stało, gdyby zostało za pętlą, a w jej środku poleciał wyjątek?

5. Czy catch (Exception $wyjatek) przechwyci TypeError (np. gdy do funkcji z parametrem int przekażesz tekst "abc")? Uzasadnij.

6. Uzupełnij i odpowiedz: w bloku catch wypisujemy użytkownikowi echo $wyjatek->________;. Których metod wyjątku nie pokazujemy na stronie i dlaczego?

Odpowiedzi
  1. ACD. throw przerywa funkcję, reszta bloku try (czyli echo "B") jest pomijana, PHP skacze do catch, a po nim program działa dalej.
  2. Brak słowa throw. Samo new Exception(...) tworzy obiekt i go porzuca — program leci dalej i dodaje ujemną nagrodę. Poprawnie: throw new Exception("...");.
  3. a) wyjątek, b) zwykła wartość — pusty plik to normalny stan („Brak zadań.”), c) wyjątek — dane uszkodzone, d) zwykła wartość true/false — zły login to nie awaria.
  4. Wyjątek przerwałby pętlę i przeskoczył do catch, więc fclose() za pętlą nigdy by się nie wykonało — plik zostałby otwarty. finally wykonuje się zawsze, także po wyjątku.
  5. Nie. TypeError należy do rodziny Error, a nie Exception — catch (Exception ...) go nie złapie. To zwykle błąd w kodzie, który poprawiamy, a nie łapiemy.
  6. getMessage(). Nie pokazujemy getFile() ani getTraceAsString() — ujawniają ścieżki na serwerze i strukturę kodu.

TaskQuest

Dodatek

Przykład z formularzem

Poniżej znajduje się przykład przedstawiający zapis danych do pliku z formularza. Formularz przesyła dane metodą POST.

Dane zapisywane do pliku są zbierane w zmiennej $info.

Plik formularz.php

<?php
// Debugowanie - wyświetl dane POST
/*
echo "<pre>";
print_r($_POST);
echo "</pre>";
*/

$info = '';
$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['przycisk'])) {
    // Walidacja
    if (empty($_POST['imie'])) {
        $errors[] = "Imię jest wymagane";
    }
    
    if (empty($_POST['nazwisko'])) {
        $errors[] = "Nazwisko jest wymagane";
    }
    
    // Jeśli są błędy, wyświetl
    if (!empty($errors)) {
        echo "<div style='color: red; border: 1px solid red; padding: 10px;'>";
        echo "<strong>Błędy:</strong><ul>";
        foreach ($errors as $error) {
            echo "<li>$error</li>";
        }
        echo "</ul></div>";
    } else {
        // Pobierz i zabezpiecz dane
        $imie = htmlspecialchars($_POST['imie'], ENT_QUOTES, 'UTF-8');
        $nazwisko = htmlspecialchars($_POST['nazwisko'], ENT_QUOTES, 'UTF-8');
        
        echo "<p>Witaj $nazwisko $imie na mojej stronie.</p>";
        
        // Zbierz informacje
        $info = "<div style='border: 1px solid #ccc; padding: 15px; margin: 10px 0;'>";
        $info .= "<h3>Przekazane dane:</h3>";
        
        // Wiadomość
        if (!empty($_POST['wiadomosc'])) {
            $wiadomosc = htmlspecialchars($_POST['wiadomosc'], ENT_QUOTES, 'UTF-8');
            $info .= "<p><strong>Wiadomość:</strong><br>" . nl2br($wiadomosc) . "</p>";
        }
        
        // Płeć
        if (!empty($_POST['plec'])) {
            $plec_map = ['k' => 'Kobieta', 'm' => 'Mężczyzna', 'i' => 'Inne'];
            $plec = $plec_map[$_POST['plec']] ?? 'Nieznana';
            $info .= "<p><strong>Płeć:</strong> $plec</p>";
        }
        
        // Gry (checkboxy pojedyncze)
        $gry = [];
        if (!empty($_POST['gra1'])) $gry[] = $_POST['gra1'];
        if (!empty($_POST['gra2'])) $gry[] = $_POST['gra2'];
        if (!empty($_POST['gra3'])) $gry[] = $_POST['gra3'];
        
        if (!empty($gry)) {
            $info .= "<p><strong>Ulubione gry:</strong> " . 
                     implode(', ', array_map('htmlspecialchars', $gry)) . "</p>";
        }
        
        // Jedzenie (checkboxy jako tablica)
        if (!empty($_POST['food']) && is_array($_POST['food'])) {
            $food = array_map('htmlspecialchars', $_POST['food']);
            $info .= "<p><strong>Ulubione jedzenie:</strong> " . 
                     implode(', ', $food) . "</p>";
        }
        
        // Klasa (select)
        if (!empty($_POST['klasa'])) {
            $klasa = htmlspecialchars($_POST['klasa']);
            $info .= "<p><strong>Klasa:</strong> $klasa</p>";
        }
        
        // Hobby (select multiple)
        if (!empty($_POST['hobby']) && is_array($_POST['hobby'])) {
            $hobby = array_map('htmlspecialchars', $_POST['hobby']);
            $info .= "<p><strong>Hobby:</strong> " . 
                     implode(', ', $hobby) . "</p>";
        }
        
        $info .= "<p><em>Data zapisu: " . date('Y-m-d H:i:s') . "</em></p>";
        $info .= "</div>";
        
        echo $info;
        
        // Zapis do pliku
        $file = "dane.txt";
        $f = fopen($file, 'a');
        
        if ($f !== false) {
            flock($f, LOCK_EX);
            fwrite($f, strip_tags($info) . "\n" . str_repeat('-', 80) . "\n");
            fflush($f);
            flock($f, LOCK_UN);
            fclose($f);
            
            echo "<p style='color: green;'>Dane zapisane do pliku!</p>";
        } else {
            echo "<p style='color: red;'>Błąd zapisu do pliku</p>";
        }
    }
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Formularz</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        form {
            background: #f5f5f5;
            padding: 20px;
            border-radius: 5px;
        }
        label {
            display: block;
            margin-top: 10px;
            font-weight: bold;
        }
        input[type="text"],
        textarea,
        select {
            width: 100%;
            padding: 8px;
            margin-top: 5px;
            box-sizing: border-box;
        }
        button {
            margin-top: 20px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover {
            background: #0056b3;
        }
        .checkbox-group,
        .radio-group {
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <h1>Formularz rejestracyjny</h1>
    
    <form method="POST">
        <label for="nazwisko">Nazwisko: *</label>
        <input type="text" id="nazwisko" name="nazwisko" required>
        
        <label for="imie">Imię: *</label>
        <input type="text" id="imie" name="imie" required>
        
        <label for="wiadomosc">Wiadomość:</label>
        <textarea id="wiadomosc" name="wiadomosc" rows="5"></textarea>
        
        <div class="radio-group">
            <label>Płeć:</label>
            <input type="radio" name="plec" value="k" id="plec_k">
            <label for="plec_k" style="display: inline; font-weight: normal;">Kobieta</label>
            
            <input type="radio" name="plec" value="m" id="plec_m">
            <label for="plec_m" style="display: inline; font-weight: normal;">Mężczyzna</label>
            
            <input type="radio" name="plec" value="i" id="plec_i">
            <label for="plec_i" style="display: inline; font-weight: normal;">Inne</label>
        </div>
        
        <div class="checkbox-group">
            <label>Ulubione gry:</label>
            <input type="checkbox" name="gra1" value="CS" id="gra1">
            <label for="gra1" style="display: inline; font-weight: normal;">CS</label>
            
            <input type="checkbox" name="gra2" value="LOL" id="gra2">
            <label for="gra2" style="display: inline; font-weight: normal;">LOL</label>
            
            <input type="checkbox" name="gra3" value="WOW" id="gra3">
            <label for="gra3" style="display: inline; font-weight: normal;">WOW</label>
        </div>
        
        <div class="checkbox-group">
            <label>Ulubione jedzenie:</label>
            <input type="checkbox" name="food[]" value="pizza" id="food1">
            <label for="food1" style="display: inline; font-weight: normal;">Pizza</label>
            
            <input type="checkbox" name="food[]" value="bigos" id="food2">
            <label for="food2" style="display: inline; font-weight: normal;">Bigos</label>
            
            <input type="checkbox" name="food[]" value="jajecznica" id="food3">
            <label for="food3" style="display: inline; font-weight: normal;">Jajecznica</label>
            
            <input type="checkbox" name="food[]" value="lody" id="food4">
            <label for="food4" style="display: inline; font-weight: normal;">Lody</label>
        </div>
        
        <label for="klasa">Klasa:</label>
        <select name="klasa" id="klasa">
            <option value="">-- Wybierz --</option>
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
        </select>
        
        <label for="hobby">Hobby (przytrzymaj Ctrl aby wybrać wiele):</label>
        <select name="hobby[]" id="hobby" multiple size="4">
            <option value="IT">IT</option>
            <option value="PHP">PHP</option>
            <option value="sport">Sport</option>
            <option value="ryby">Wędkarstwo</option>
        </select>
        
        <button type="submit" name="przycisk">Wyślij</button>
    </form>
    
    <p style="margin-top: 20px;">
        <a href="podglad.php" target="_blank">📄 Podgląd zapisanych danych</a>
    </p>
</body>
</html>

Plik podglad.php

<?php
$file = 'dane.txt';

if (!file_exists($file) || filesize($file) == 0) {
    echo "Brak zapisanych danych.";
    echo "<br><br><a href='formularz.php'>Dodaj dane</a>";
    exit;
}

$zawartosc = file_get_contents($file);
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Podgląd danych</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 900px;
            margin: 50px auto;
            padding: 20px;
        }
        .content {
            background: #f9f9f9;
            padding: 20px;
            border: 1px solid #ddd;
            white-space: pre-wrap;
        }
        .buttons {
            margin-top: 20px;
        }
        a {
            display: inline-block;
            padding: 10px 20px;
            margin-right: 10px;
            background: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        a:hover {
            background: #0056b3;
        }
        .delete {
            background: #dc3545;
        }
        .delete:hover {
            background: #c82333;
        }
    </style>
</head>
<body>
    <h1>📄 Zapisane dane</h1>
    
    <div class="content">
        <?= nl2br(htmlspecialchars($zawartosc)) ?>
    </div>
    
    <div class="buttons">
        <a href="formularz.php">➕ Dodaj kolejne dane</a>
        <a href="del.php" class="delete" 
           onclick="return confirm('Czy na pewno usunąć wszystkie dane?')">
            🗑️ Usuń plik
        </a>
    </div>
</body>
</html>

Plik del.php

<?php
$file = 'dane.txt';

if (file_exists($file)) {
    if (unlink($file)) {
        $message = "Plik został usunięty";
        $status = "success";
    } else {
        $message = "Błąd podczas usuwania pliku";
        $status = "error";
    }
} else {
    $message = "Plik nie istnieje";
    $status = "warning";
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Usuwanie pliku</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
            text-align: center;
        }
        .message {
            padding: 20px;
            margin: 20px 0;
            border-radius: 5px;
            font-size: 18px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .warning {
            background: #fff3cd;
            color: #856404;
            border: 1px solid #ffeeba;
        }
        a {
            display: inline-block;
            margin-top: 20px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        a:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>
    <div class="message <?= $status ?>">
        <?= $message ?>
    </div>
    
    <a href="formularz.php">Powrót do formularza</a>
</body>
</html>

Przykład – upload plików

Formularz uploadu

<!-- WAŻNE: enctype="multipart/form-data" -->
<form method="POST" enctype="multipart/form-data">
    <label>Wybierz plik:</label>
    <input type="file" name="plik" accept="image/*,application/pdf">
    
    <button type="submit" name="upload">Prześlij</button>
</form>

Obsługa uploadu

<?php
if (isset($_POST['upload']) && isset($_FILES['plik'])) {
    $file = $_FILES['plik'];
    
    // Sprawdź błędy
    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo "Błąd uploadu: " . $file['error'];
        exit;
    }
    
    // Sprawdź rozmiar (5MB max)
    $maxSize = 5 * 1024 * 1024;
    if ($file['size'] > $maxSize) {
        echo "Plik za duży (max 5MB)";
        exit;
    }
    
    // Sprawdź rozszerzenie
    $allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($ext, $allowedExt)) {
        echo "Niedozwolone rozszerzenie: $ext";
        exit;
    }
    
    // Utwórz katalog jeśli nie istnieje
    $uploadDir = 'uploads/';
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
    }
    
    // Generuj unikalną nazwę
    $newName = uniqid() . '.' . $ext;
    $destination = $uploadDir . $newName;
    
    // Przenieś plik
    if (move_uploaded_file($file['tmp_name'], $destination)) {
        echo "Plik przesłany: $destination";
    } else {
        echo "Błąd przenoszenia pliku";
    }
}
?>

Zadania:

W każdym zadaniu należy sprawdzić, czy dane istnieją. Używaj metody bezpiecznej, chyba że rodzaj zadania wymaga innego podejścia.

Zadanie 1: Stwórz formularz, który na podstawie podanych przez użytkownika ilości kolumn i wierszy, narysuje tabelę w HTMLu. Wypełnij pola tabeli losowymi kolorami.

Zadanie 2: Stwórz formularz, który po kliknięciu przycisku będzie losował liczby totolotka (zakres 1-49), umożliw podanie użytkownikowi ilości zestawów do wygenerowania. (np. wpisuje 3 – więc 3 razy generuje się zestaw itd.).

Zadanie 3: Stwórz formularz, który będzie zawierał listę rzeczy do kupienia. W tabeli wyświetl produkt, ilość i cenę. Produkt ma zawierać checkboxa oraz nazwę; ilość i cena mogą być edytowane (po pierwszym uruchomieniu wypełnione dowolnymi wartościami – ty decydujesz). Zadeklaruj przynajmniej 6 produktów do wyboru. Po wybraniu produktów (zaznaczeniu checkboxów) i wysłaniu formularza wyświetli się paragon, tak jak w sklepie (poszukaj jakiegoś w plecaku i wykorzystaj za wzór).

Zadanie 4: Stwórz grę „papier, kamień, nożyce”. Informację o wyniku przechowuj w zmiennej sesyjnej. Jeżeli nie znasz jeszcze sesji to możesz wynik danego gracza przekazywać w polu ukrytym

np. <input type=”hidden” name=”licznik_player” value=”<?=$licznik_player?>”>

Zadanie 5: Stwórz rozbudowany formularz danych użytkownika w PHP, który umożliwia:

  1. zapisanie wprowadzonych danych do pliku tekstowego,
  2. odczytanie zapisanych danych i ich czytelne wyświetlenie w przeglądarce.

Formularz powinien zawierać m.in.:

Dane osobowe

  • Imię i nazwisko (pole tekstowe)

  • Adres e-mail (pole typu email)

  • Hasło (pole typu password)

  • Numer telefonu (pole typu tel)

  • Data urodzenia (pole typu date)

  • Wiek (pole typu number)

  • Płeć (radio button: mężczyzna / kobieta / inne)

  • Zdjęcie profilowe (upload pliku) – opcjonalne

Dane adresowe

  • Ulica i numer domu

  • Kod pocztowy

  • Miasto

  • Województwo (lista rozwijana select)

  • Kraj (lista rozwijana select)

  • Współrzędne GPS (pole tekstowe)

Dane zawodowe

  • Stanowisko

  • Nazwa firmy

  • Doświadczenie zawodowe (liczba lat)

  • Branża (lista rozwijana z możliwością wyboru kilku opcji – multiple)

  • Przedział wynagrodzenia (range)

Preferencje i zainteresowania

  • Ulubione kolory (color)

  • Ulubione dni tygodnia (checkbox)

  • Ulubiona godzina (time)

  • Ulubiony miesiąc (month)

  • Adres strony internetowej (url)

  • Krótki opis zainteresowań (textarea)

  • Upload pliku CV (file) – opcjonalnie

  • Zgoda na newsletter (checkbox)

  • Ocena poziomu satysfakcji (range)

Inne elementy

  • Pole ukryte (hidden) – np. data wysłania formularza

  • Przycisk wyczyszczenia formularza (reset)

  • Przycisk wysyłania (submit)


Zapis danych do pliku

Po przesłaniu formularza dane użytkownika powinny zostać zapisane do pliku tekstowego (np. dane.txt).
Każdy wiersz w pliku powinien zawierać dane jednego użytkownika, rozdzielone średnikami (;) — np.:

Jan Kowalski;jan.kowalski@example.com;35;mężczyzna;Warszawa;Programista;IT;#3498db;Tak;2025-10-27

Do zapisu możesz wykorzystać funkcje PHP takie jak:

  • fopen(),

  • fputcsv() – poczytaj o tej funkcji w dokumentacji technicznej

  • fclose().


Odczyt danych (dane.php)

Dane powinny wyświetlać się w formie tabeli HTML. Tabela powinna:

  • mieć nagłówki kolumn,

  • prezentować wszystkie zapisane dane użytkowników,

  • wyświetlać komunikat „Brak zapisanych danych”, jeśli plik jest pusty.


Wymagania techniczne

  • Walidacja pól formularza po stronie HTML5 (np. required, min, max, pattern).

  • Dane powinny być zabezpieczone przed wstrzyknięciami HTML (np. za pomocą htmlspecialchars()).

  • Użytkownik po przesłaniu formularza powinien zobaczyć komunikat o poprawnym zapisie danych.

  • Pliki przesyłane przez użytkownika (np. zdjęcie, CV) powinny być zapisywane w katalogu uploads/.

TaskQuest 1.3 — użytkownik dodaje zadania ze strony

KARTA PRACY

Po Lekcji 12 TaskQuest bezpiecznie wczytuje zadania z data/zadania.csv i głośno zgłasza awarie. Ale żeby dodać zadanie, Kama nadal musi otworzyć plik CSV w edytorze kodu i ręcznie wpisać wiersz — pilnując średników, cudzysłowów, formatu daty i kolejnego id. Żaden użytkownik prawdziwej aplikacji tak nie pracuje.

Dziś TaskQuest dostaje pierwszy formularz: użytkownik wpisuje zadanie na stronie, klika „Dodaj zadanie”, a PHP sprawdza dane i zapisuje je do pliku. Przy okazji lista zadań nauczy się filtrowania przez adres strony (?filtr=aktywne).

Cel lekcji

Nauczyć TaskQuest:

  • odbierać dane z formularza HTML ($_POST) i z adresu strony ($_GET),
  • odróżniać pierwsze wejście na stronę od wysłania formularza ($_SERVER['REQUEST_METHOD']),
  • bezpiecznie sięgać po dane, których może nie być (operator ??),
  • sprawdzać dane od użytkownika po stronie serwera i pokazywać wszystkie błędy naraz,
  • dopisywać nowe zadanie do pliku (wczytaj → dodaj → zapisz) z nowym, unikalnym id,
  • nie dodawać tego samego zadania dwa razy po odświeżeniu strony (przekierowanie po zapisie),
  • świadomie wybierać metodę: GET do czytania, POST do zmieniania danych.

Efekt: na stronie głównej jest link „+ Dodaj zadanie” prowadzący do nowej strony dodaj-zadanie.php. Po poprawnym wypełnieniu formularza zadanie ląduje w data/zadania.csv, użytkownik wraca na stronę główną i widzi komunikat „Zadanie zostało dodane.”. Lista zadań ma trzy widoki: wszystkie, aktywne i wykonane.

Uwaga — co jest, a czego jeszcze nie ma:

  • Nie ma logowania ani sesji. Komunikat „Zadanie zostało dodane.” przekazujemy w adresie strony (?dodano=1), a nie „w pamięci” aplikacji — sesje to Lekcja 14.
  • Nadal nie używamy htmlspecialchars(). Od dziś tekst wpisany przez użytkownika trafia na stronę, więc to już realny problem bezpieczeństwa — zobaczysz go w Eksperymencie 5, a naprawimy go w lekcji o bezpieczeństwie (22). Aplikacja działa na Twoim komputerze i tylko Ty wpisujesz dane — dlatego na razie możemy to odłożyć.
  • Formularz tylko dodaje zadania. Oznaczanie jako wykonane to zadanie końcowe A; edycja i usuwanie — przy bazie danych (Lekcja 19).
  • Przesyłanie plików (upload) i pozostałe typy pól (checkbox, radio, select) poznasz w ćwiczeniach z tematu, ale TaskQuest ich dziś nie potrzebuje.
  • Dane grywalizacji ($xp, $streak, $dataOstatniejAktywnosci) nadal są symulowane w index.php.

Rozgrzewka — 4 pytania z Lekcji 12

Odpowiedz z pamięci, zanim zaczniesz:

  1. W bloku try są trzy linie kodu, a wyjątek poleciał w drugiej. Co stanie się z trzecią linią i dokąd „przeskoczy” PHP?
  2. Dlaczego pusty plik z zadaniami nie rzuca wyjątku, a brak pliku — rzuca?
  3. Po co w index.php jest $zadania = []; przed blokiem try?
  4. Dokończ regułę: „Kto wykrywa problem — …, kto wie, co z problemem zrobić — …”.

1. Wracamy do naszego TaskQuest

Aplikacja czyta zadania z pliku, a funkcja zapiszZadania() istnieje od Lekcji 11 — ale nikt jej jeszcze nie wywołuje ze strony. Dziś wreszcie dostanie swoje miejsce w aplikacji. Wszystkie zmienne i funkcje z Lekcji 12 zostają bez zmian.

Docelowa struktura projektu

taskquest/
├── index.php                        (zmiana: link do formularza, komunikat po dodaniu, filtr GET)
├── dodaj-zadanie.php                NOWY — formularz dodawania zadania + jego obsługa
├── diagnostyka.php                  (zmiana: jedna nowa pozycja na liście wymaganych plików)
├── config/
│   └── config.php                   bez zmian
├── includes/
│   ├── header.php                   (zmiana: klasa .sukces + style formularza)
│   ├── footer.php                   (zmiana: "TaskQuest 1.3")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         bez zmian
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            (zmiana: dwie nowe funkcje na końcu pliku)
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           bez zmian
└── data/
    └── zadania.csv                  (zmieni się jego zawartość — dopiszą się zadania z formularza)

Co gdzie trafi

PlikCo się zmienia
includes/funkcje_zadan.phpnowe funkcje walidujNoweZadanie() i nastepneIdZadania()
dodaj-zadanie.phpnowa strona: formularz, sprawdzenie danych, zapis, przekierowanie
index.phplink „+ Dodaj zadanie”, komunikat o dodaniu, wybór widoku listy przez $_GET
includes/header.phpklasa .sukces i kilka reguł dla formularza
includes/footer.php, diagnostyka.phpnumer wersji, nowy plik na liście wymaganych

Ciekawostka: w nowej stronie wrócą nazwy $tytulZadania, $opisZadania i $terminZadania, które usunęliśmy w Lekcji 9 (dane zadań przeniosły się wtedy do kluczy tablicy $zadania). Teraz dostają nowe, konkretne znaczenie: dane nowego zadania wpisane w formularzu, zanim trafią do listy.

Przygotowanie (zrób to teraz)

  1. Zrób kopię zapasową całego katalogu taskquest/ z Lekcji 12 — np. jako taskquest_lekcja12/ (obok, nie w środku). Przyda się do testu regresji.
  2. Zrób kopię pliku data/zadania.csv poza katalogiem taskquest/ (np. na pulpicie, jako zadania_kopia.csv). Dziś będziemy dopisywać do niego testowe zadania, a na koniec wrócimy do oryginału.
  3. Sprawdź, czy index.php pokazuje 5 zadań, a diagnostyka.php — 11 × [OK].

2. Problem w aplikacji

Kama chce dodać zadanie „Powtórka z formularzy” na przyszły piątek, za 30 XP. Dziś musi:

  1. otworzyć data/zadania.csv w edytorze,
  2. sprawdzić, jakie jest największe id, i wymyślić kolejne,
  3. wpisać wiersz, nie gubiąc żadnego średnika i pamiętając o formacie RRRR-MM-DD,
  4. odświeżyć stronę i mieć nadzieję, że niczego nie zepsuła (jeśli zepsuła — od Lekcji 12 przynajmniej zobaczy ramkę z numerem linii).

Chcemy, żeby wyglądało to tak:

formularz w przeglądarce ──(kliknięcie "Dodaj zadanie")──► PHP odbiera dane
                                   │
                         czy dane są poprawne?
                      ┌────────────┴────────────┐
                     NIE                        TAK
                      │                          │
      formularz jeszcze raz,            wczytajZadania() → dopisz → zapiszZadania()
       z listą błędów                           │
       i wpisanymi danymi            powrót na stronę główną
                                     + "Zadanie zostało dodane."

Do tego Kama ma już 5 zadań i chciałaby czasem widzieć tylko aktywne. To zupełnie inny rodzaj prośby: niczego nie zmienia w danych, tylko wybiera, co pokazać.

Potrzebujemy więc:

  1. sposobu, żeby przeglądarka wysłała dane do skryptu PHP,
  2. sposobu, żeby PHP te dane odebrał,
  3. sposobu, żeby PHP odróżnił „ktoś dopiero otworzył formularz” od „ktoś go właśnie wysłał”,
  4. sprawdzenia danych — przeglądarka potrafi wysłać wszystko,
  5. dwóch rodzajów próśb: „pokaż mi” i „zmień dane”.

To właśnie formularze HTML i metody GET oraz POST.

3. Nowy mechanizm PHP: formularze, GET i POST

3.1 Jak przeglądarka rozmawia z PHP

Za każdym razem, gdy wpisujesz adres, klikasz link albo wysyłasz formularz, przeglądarka wysyła do serwera żądanie (ang. request). Serwer uruchamia skrypt PHP od zera, skrypt generuje HTML, a serwer odsyła go jako odpowiedź (ang. response). Potem skrypt się kończy i wszystkie jego zmienne znikają.

PRZEGLĄDARKA                                 SERWER (Apache + PHP)
     │  żądanie: GET /taskquest/index.php          │
     │ ──────────────────────────────────────────► │  uruchamia index.php od początku
     │                                             │  (zmienne powstają...)
     │  odpowiedź: HTML strony                     │
     │ ◄────────────────────────────────────────── │  (...i znikają po zakończeniu)

Każde żądanie ma metodę. Dwie najważniejsze to:

  • GET — „daj mi stronę”. Tak działa wpisanie adresu i kliknięcie linku. Dane (jeśli są) jadą w adresie, po znaku ?.
  • POST — „przyjmij ode mnie dane”. Dane jadą w treści żądania, nie w adresie.

3.2 Pierwszy formularz — metoda GET

BRUDNOPIS. Utwórz w katalogu taskquest/ plik brudnopis.php — to plik do eksperymentów, nie część aplikacji. Cała sekcja 3 i 4 dzieje się w tym pliku.

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Brudnopis — formularz</title>
</head>
<body>
    <form method="get" action="brudnopis.php">
        <label for="imie">Imię</label>
        <input type="text" id="imie" name="imie">

        <label for="xp">XP</label>
        <input type="number" id="xp" name="xp">

        <button type="submit">Wyślij</button>
    </form>

    <pre><?php var_dump($_GET); ?></pre>
</body>
</html>

Otwórz http://localhost/taskquest/brudnopis.php, wpisz Kama i 50, kliknij „Wyślij”. Zwróć uwagę na trzy rzeczy:

  1. Adres zmienił się na brudnopis.php?imie=Kama&xp=50. Dane są widoczne w pasku adresu.
  2. var_dump($_GET) pokazuje tablicę asocjacyjną: ["imie"] => "Kama", ["xp"] => "50".
  3. xp to string(2) "50", a nie liczba — mimo że pole ma type="number". Z formularza zawsze przychodzą teksty.

Co tu się dzieje:

  • method="get" — przeglądarka dokleja dane do adresu,
  • action="brudnopis.php" — do którego skryptu wysłać dane (tu: do tego samego pliku),
  • atrybut name pola staje się kluczem w tablicy $_GET. Pole bez name w ogóle nie zostanie wysłane (atrybut id służy tylko do połączenia z <label for="...">),
  • $_GET to tablica, którą PHP wypełnia sam, zanim uruchomi Twój kod.

Teraz wpisz adres ręcznie, bez formularza: brudnopis.php?imie=Ola&xp=abc&bonus=1. PHP przyjął wszystko — także abc w polu liczbowym i dodatkowy klucz bonus, którego w formularzu nie ma. Zapamiętaj: formularz to tylko jeden ze sposobów wysłania danych. Użytkownik może wysłać, co zechce.

Na koniec otwórz brudnopis.php bez niczego po ?. $_GET jest pustą tablicą: array(0) {}.

3.3 Metoda POST

Zmień w formularzu method="get" na method="post", a var_dump($_GET) na var_dump($_POST). Wyślij formularz jeszcze raz.

  • Adres się nie zmienił — nie ma w nim ?imie=....
  • Dane są w $_POST, zbudowanej dokładnie tak samo jak $_GET.

Gdzie się podziały? Otwórz narzędzia deweloperskie (F12) → karta Sieć (Network) → wyślij formularz → kliknij żądanie brudnopis.php → karta Ładunek (Payload). Tam są. POST nie jest „tajny” — dane po prostu nie jadą w adresie. Każdy, kto ma dostęp do przeglądarki albo sieci, może je zobaczyć lub zmienić. (Szyfrowanie połączenia zapewnia HTTPS, nie metoda POST.)

Teraz naciśnij F5. Przeglądarka pyta, czy wysłać dane ponownie. Zapamiętaj to okienko — w TaskQuest oznaczałoby drugie takie samo zadanie w pliku. Wrócimy do tego w Kroku 4.

3.4 Pierwsze wejście czy wysłany formularz?

Ten sam plik brudnopis.php jest otwierany na dwa sposoby: najpierw zwykłym wejściem (GET — pusty formularz), potem po kliknięciu przycisku (POST — dane do sprawdzenia). PHP musi je odróżnić. Podmień zawartość brudnopis.php na:

<?php

echo "Metoda: " . $_SERVER['REQUEST_METHOD'] . "<br>";
echo "Imię: " . $_POST['imie'] . "<br>";
?>
<form method="post" action="brudnopis.php">
    <input type="text" name="imie">
    <button type="submit">Wyślij</button>
</form>

Przy pierwszym wejściu zobaczysz Metoda: GET oraz ostrzeżenie Warning: Undefined array key "imie" — przy zwykłym wejściu klucza imie w $_POST nie ma. Po wysłaniu: Metoda: POST i wpisane imię.

Dwa narzędzia, które to porządkują:

1. $_SERVER['REQUEST_METHOD'] — tekst 'GET' albo 'POST'. Kod obsługujący formularz umieszczamy w:

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // formularz został wysłany — dopiero tu sięgamy po $_POST
}

2. Operator ?? — „weź wartość z lewej, a jeśli jej nie ma (klucz nie istnieje albo jest null), weź tę z prawej”:

$imie = $_POST['imie'] ?? '';

To krótszy zapis tego samego, co:

if (isset($_POST['imie'])) {
    $imie = $_POST['imie'];
} else {
    $imie = '';
}

Nawet wewnątrz if (... === 'POST') używamy ?? — bo ktoś może wysłać POST bez jakiegoś pola (np. usunąć je z formularza w F12). Zmień drugą linię na echo "Imię: " . ($_POST['imie'] ?? '(brak)') . "<br>"; i sprawdź pierwsze wejście — ostrzeżenie zniknęło.

3.5 GET czy POST?

ProśbaMetodaDlaczego
pokaż listę zadań / tylko aktywneGETniczego nie zmienia; adres z ?filtr=aktywne można zapisać w zakładkach, wysłać koledze, odświeżyć bez skutków ubocznych
wyszukaj zadania ze słowem „PHP”GETj.w. — to tylko czytanie
dodaj zadaniePOSTzmienia dane; odświeżenie, link w historii albo robot indeksujący stronę nie mogą przypadkiem dodać zadania
oznacz zadanie jako wykonanePOSTzmienia dane
logowaniePOSThasło w adresie trafiłoby do historii przeglądarki i logów serwera

Reguła na cały kurs:

GET — gdy tylko czytamy. POST — gdy coś zmieniamy.

3.6 Pola formularza i walidacja w przeglądarce

W formularzu TaskQuest użyjemy czterech rodzajów pól:

PoleCo wysyłaPrzydatne atrybuty
<input type="text">tekstrequired, maxlength="100"
<textarea>tekst, może mieć wiele linii (wartość wpisujemy między znacznikami, nie w value)rows, maxlength
<input type="date">zawsze tekst RRRR-MM-DD — niezależnie od tego, jak przeglądarka wyświetla datę (np. 02.10.2026)required
<input type="number">tekst z liczbąmin, max, required

Atrybuty required, min, max, maxlength to walidacja w przeglądarce: przeglądarka nie wyśle formularza, dopóki pola nie będą poprawne. To wygoda dla użytkownika (szybka informacja bez przeładowania strony), ale nie ochrona. Obejść ją można na kilka sposobów:

  • dopisać do znacznika <form> atrybut novalidate — przeglądarka przestaje sprawdzać pola (użyjemy go w testach),
  • usunąć atrybut w narzędziach deweloperskich (F12 → Elementy),
  • wysłać dane bez przeglądarki (np. programem curl) albo — przy GET — po prostu wpisać adres.

Dlatego PHP zawsze sprawdza dane sam, nawet jeśli formularz ma required.

3.7 Błąd użytkownika to nie awaria

W Lekcji 12 ustaliliśmy: wyjątek to sygnał „nie potrafię zrobić tego, o co mnie poproszono”, a zły login to zwykła odpowiedź (false), nie awaria. Z formularzem jest tak samo:

SytuacjaJak ją zgłaszamyDlaczego
pusty tytuł, XP = 500, termin 31 lutegolista komunikatów (tablica $bledy)to oczekiwana sytuacja — ludzie się mylą; formularz ma im pomóc to poprawić
plik z zadaniami nie istnieje, nie da się go zapisaćwyjątek (try / catch)aplikacja nie może wykonać zadania

Jest też powód praktyczny: wyjątek przerywa sprawdzanie na pierwszym problemie. Użytkownik, który zrobił trzy błędy, poprawiałby je wtedy po jednym, wysyłając formularz trzy razy. Tablica błędów pozwala pokazać wszystkie naraz:

$bledy = [];

if (/* tytuł pusty */) {
    $bledy[] = "Tytuł jest wymagany.";
}

if (/* XP złe */) {
    $bledy[] = "XP musi być z zakresu 1–100.";
}

if (count($bledy) === 0) {
    // wszystko w porządku — można zapisywać
}

Typowe błędy przy formularzach

  • Brak atrybutu name w polu — pole nie zostanie wysłane, a w PHP pojawi się Undefined array key.
  • Sięganie po $_POST['...'] bez ?? przy pierwszym wejściu na stronę — ostrzeżenie Undefined array key.
  • Traktowanie danych z formularza jak liczb — $_POST['xp'] to zawsze tekst. (int) "12abc" daje 12, a (int) "dużo" daje 0 — rzutowanie „po cichu” zamienia śmieci w liczbę.
  • Poleganie tylko na required i min/max — przeglądarkę da się ominąć.
  • Użycie GET do zmiany danych — link lub odświeżenie strony zmienia dane bez wiedzy użytkownika.
  • Wpisanie wartości <textarea> w atrybut value — textarea nie ma atrybutu value, tekst wpisuje się między <textarea> a </textarea>.
  • Literówka w nazwie: name="tytul" w HTML, a $_POST['tytuł'] albo $_POST['title'] w PHP — klucze muszą być identyczne.

4. SAMODZIELNIE — formularz, który sprawdza dane

BRUDNOPIS. To ostatnie ćwiczenie w brudnopis.php, zanim zaczniemy zmieniać projekt. Zastąp zawartość pliku kodem, który:

  1. wyświetla formularz (metoda POST, wysyłany do brudnopis.php) z dwoma polami: tytul i xp — oba jako type="text" i bez required, żeby przeglądarka niczego nie blokowała i całą pracę musiał wykonać PHP,
  2. przy wysłaniu formularza (i tylko wtedy!) sprawdza dane:
    • tytuł (po trim()) nie może być pusty → Tytuł jest wymagany.,
    • XP musi składać się z samych cyfr → XP musi być liczbą całkowitą., a jeśli jest liczbą, musi być z zakresu 1–100 → XP musi być z zakresu 1–100.,
  3. gdy są błędy — wypisuje je jako listę <ul>, gdy ich nie ma — wypisuje Przyjęto zadanie: <tytuł> (<xp> XP),
  4. po wysłaniu formularz nie jest pusty — pola zawierają to, co wpisał użytkownik (żeby przy błędzie nie musiał pisać wszystkiego od nowa).

Efekt:

Wpisane daneOczekiwany wynik
pierwsze wejście (GET)sam pusty formularz, brak komunikatów, brak ostrzeżeń PHP
tytuł pusty, XP pustedwa błędy: tytuł i „liczbą całkowitą”
Nauka, 150jeden błąd: zakres 1–100; w polu tytułu nadal Nauka
, abcdwa błędy (same spacje to pusty tytuł)
Nauka PHP, 30Przyjęto zadanie: Nauka PHP (30 XP)

Jak sprawdzić: każdy wiersz tabeli. Szczególnie pierwszy — jeśli przy pierwszym wejściu widzisz Warning, sięgasz po $_POST za wcześnie albo bez ??.

HINT

Kod PHP ustaw na górze pliku, przed HTML. Zmienne $tytulZadania, $xpZadania i $bledy muszą istnieć także przy pierwszym wejściu — nadaj im wartości początkowe (puste) przed if. Dzięki temu w HTML możesz ich zawsze bezpiecznie użyć, np. w atrybucie value. Do sprawdzenia „samych cyfr” masz wzorzec z Lekcji 12.

POMOC
<?php

$tytulZadania = '';
$xpZadania = '';
$bledy = [];
$przyjete = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytulZadania = trim($_POST['tytul'] ?? '');
    // ... to samo dla xp ...

    // ... sprawdzenia, każde dopisuje komunikat do $bledy ...

    $przyjete = count($bledy) === 0;
}
?>
<!-- HTML: komunikat "Przyjęto" (gdy $przyjete), lista błędów (gdy są), formularz -->
<input type="text" id="tytul" name="tytul" value="<?= $tytulZadania; ?>">

Listę błędów zbudujesz pętlą foreach wewnątrz <ul> — tak samo jak wchodzisz i wychodzisz z PHP przy if w index.php: <?php foreach (...) { ?> ... <?php } ?>.

ROZWIĄZANIE
<?php

$tytulZadania = '';
$xpZadania = '';
$bledy = [];
$przyjete = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytulZadania = trim($_POST['tytul'] ?? '');
    $xpZadania = trim($_POST['xp'] ?? '');

    if ($tytulZadania === '') {
        $bledy[] = "Tytuł jest wymagany.";
    }

    if (!preg_match('/^\d+$/', $xpZadania)) {
        $bledy[] = "XP musi być liczbą całkowitą.";
    } elseif ((int) $xpZadania < 1 || (int) $xpZadania > 100) {
        $bledy[] = "XP musi być z zakresu 1–100.";
    }

    $przyjete = count($bledy) === 0;
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Brudnopis — formularz</title>
</head>
<body>
    <?php if ($przyjete) { ?>
    <p>Przyjęto zadanie: <?= $tytulZadania; ?> (<?= $xpZadania; ?> XP)</p>
    <?php } ?>

    <?php if (count($bledy) > 0) { ?>
    <ul>
        <?php foreach ($bledy as $blad) { ?>
        <li><?= $blad; ?></li>
        <?php } ?>
    </ul>
    <?php } ?>

    <form method="post" action="brudnopis.php">
        <label for="tytul">Tytuł</label>
        <input type="text" id="tytul" name="tytul" value="<?= $tytulZadania; ?>">

        <label for="xp">XP</label>
        <input type="text" id="xp" name="xp" value="<?= $xpZadania; ?>">

        <button type="submit">Sprawdź</button>
    </form>
</body>
</html>

Dlaczego XP sprawdzamy najpierw regexem, a dopiero potem zakresem (elseif)? Bo (int) "12abc" to 12 — przeszłoby sprawdzenie zakresu, choć użytkownik wpisał bzdurę. A (int) "dużo" to 0 — użytkownik dostałby mylący komunikat o zakresie zamiast informacji, że to w ogóle nie jest liczba. Sprawdzisz to w Eksperymencie 4.

Ten schemat — wartości początkowe → if (POST) → odczyt z ?? → sprawdzenia do tablicy $bledy → HTML z listą błędów i wypełnionym formularzem — powtórzymy za chwilę w TaskQuest.

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

PROJEKT. Od tego miejsca kończymy pracę w brudnopis.php (zostaw go na razie — przyda się w testach) i zmieniamy pliki prawdziwej aplikacji w katalogu taskquest/. Kolejność: najpierw funkcje (logika bez HTML), potem nowa strona, na końcu połączenie jej ze stroną główną.

Krok 1 — SAMODZIELNIE: walidujNoweZadanie()

Plik projektu: includes/funkcje_zadan.php. Na końcu pliku (pod sortujPoTerminie()) dopisz funkcję:

function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array

Funkcja dostaje już oczyszczone teksty z formularza i zwraca tablicę komunikatów o błędach. Pusta tablica oznacza: dane są poprawne. Funkcja niczego nie wyświetla i nie rzuca wyjątków (patrz 3.7).

PoleRegułaKomunikat (dokładnie taki)
tytułnie może być pustyTytuł jest wymagany.
tytułnajwyżej 100 znaków (sprawdzaj tylko, gdy nie jest pusty)Tytuł może mieć najwyżej 100 znaków.
opisnieobowiązkowy, ale najwyżej 300 znakówOpis może mieć najwyżej 300 znaków.
terminkształt RRRR-MM-DDTermin musi być datą w formacie RRRR-MM-DD.
termindzień musi istnieć w kalendarzu (sprawdzaj tylko, gdy kształt jest poprawny)Taki dzień nie istnieje w kalendarzu.
XPsame cyfryXP musi być liczbą całkowitą.
XPzakres 1–100 (sprawdzaj tylko, gdy to liczba)XP musi być z zakresu 1–100.

Długość tekstu licz przez mb_strlen() (Lekcja 8) — strlen() policzyłby polskie litery podwójnie.

Do sprawdzenia, czy dzień istnieje, użyj wbudowanej funkcji checkdate($miesiac, $dzien, $rok) — przyjmuje trzy liczby całkowite i zwraca true albo false (np. checkdate(2, 31, 2026) → false). Trzy liczby z tekstu 2026-02-31 wydobędziesz przez explode('-', $terminZadania) i rzutowanie (int). (Jeśli robiłaś/robiłeś zadanie końcowe A z Lekcji 12, znasz już ten pomysł.)

Zauważ: w komunikatach nie powtarzamy tego, co wpisał użytkownik (np. „Termin 2026-02-31 …”). Tekst od użytkownika trafia na stronę tylko tam, gdzie to konieczne — dlaczego to ważne, zobaczysz w Eksperymencie 5.

Jak sprawdzić: w brudnopis.php (podmień zawartość):

<?php

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

print_r(walidujNoweZadanie('Nauka PHP', '', '2026-10-02', '20'));
print_r(walidujNoweZadanie('', '', '2026-02-31', 'dużo'));

Pierwsze wywołanie: Array ( ). Drugie: trzy komunikaty — o tytule, o kalendarzu i o liczbie całkowitej. Pełny zestaw przypadków jest w Teście 1.

HINT

Pary reguł dla jednego pola („pusty?” → „za długi?”, „kształt?” → „kalendarz?”, „cyfry?” → „zakres?”) to naturalne miejsce na if / elseif albo if / else z zagnieżdżonym if. Druga reguła ma sens tylko wtedy, gdy pierwsza przeszła. Wzorzec daty masz w sprawdzWierszZadania().

POMOC
function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array
{
    $bledy = [];

    if ($tytulZadania === '') {
        $bledy[] = "Tytuł jest wymagany.";
    } elseif (/* za długi */) {
        // ...
    }

    // opis ...

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $terminZadania)) {
        // ...
    } else {
        $czesciDaty = explode('-', $terminZadania);   // [0] => rok, [1] => miesiąc, [2] => dzień

        // checkdate(miesiąc, dzień, rok) — uwaga na kolejność!
    }

    // XP ...

    return $bledy;
}
ROZWIĄZANIE
function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array
{
    $bledy = [];

    if ($tytulZadania === '') {
        $bledy[] = "Tytuł jest wymagany.";
    } elseif (mb_strlen($tytulZadania) > 100) {
        $bledy[] = "Tytuł może mieć najwyżej 100 znaków.";
    }

    if (mb_strlen($opisZadania) > 300) {
        $bledy[] = "Opis może mieć najwyżej 300 znaków.";
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $terminZadania)) {
        $bledy[] = "Termin musi być datą w formacie RRRR-MM-DD.";
    } else {
        $czesciDaty = explode('-', $terminZadania);

        if (!checkdate((int) $czesciDaty[1], (int) $czesciDaty[2], (int) $czesciDaty[0])) {
            $bledy[] = "Taki dzień nie istnieje w kalendarzu.";
        }
    }

    if (!preg_match('/^\d+$/', $xpZadania)) {
        $bledy[] = "XP musi być liczbą całkowitą.";
    } elseif ((int) $xpZadania < 1 || (int) $xpZadania > 100) {
        $bledy[] = "XP musi być z zakresu 1–100.";
    }

    return $bledy;
}

Porównaj z sprawdzWierszZadania() z Lekcji 12: sprawdzenia są podobne (kształt daty, same cyfry), ale sposób zgłaszania inny. Tam uszkodzony plik to awaria → wyjątek. Tu pomyłka użytkownika to normalna sytuacja → lista komunikatów.

Krok 2 — SAMODZIELNIE: nastepneIdZadania()

Plik projektu: includes/funkcje_zadan.php. Nowe zadanie potrzebuje id, którego nie ma jeszcze żadne inne zadanie. Dopisz nową funkcję między sortujPoTerminie() a walidujNoweZadanie() (dla PHP kolejność funkcji w pliku nie ma znaczenia — to tylko porządek dla czytającego):

function nastepneIdZadania(array $zadania): int

Funkcja zwraca największe id na liście + 1. Dla pustej listy zwraca 1.

Zanim zaczniesz, odpowiedz: dlaczego nie count($zadania) + 1? Wyobraź sobie, że Kama ręcznie usunęła z pliku zadanie o id 3. Na liście zostały 1, 2, 4, 5 — ile wynosi count() + 1 i co by się stało?

Jak sprawdzić: w brudnopis.php:

echo nastepneIdZadania([]) . "<br>";                                        // 1
echo nastepneIdZadania([['id' => 1], ['id' => 2], ['id' => 3]]) . "<br>";   // 4
echo nastepneIdZadania([['id' => 3], ['id' => 10], ['id' => 7]]) . "<br>";  // 11

(Funkcja potrzebuje tylko klucza 'id', więc do testu wystarczą takie „okrojone” zadania.)

HINT

To klasyczne szukanie maksimum pętlą foreach — pamiętasz podobny wzorzec z sumowaniem XP w sumujXp() (Lekcja 9). Zmienna pomocnicza startuje od 0, bo id są dodatnie.

ROZWIĄZANIE
function nastepneIdZadania(array $zadania): int
{
    $najwiekszeId = 0;

    foreach ($zadania as $zadanie) {
        if ($zadanie['id'] > $najwiekszeId) {
            $najwiekszeId = $zadanie['id'];
        }
    }

    return $najwiekszeId + 1;
}

Odpowiedź: dla 1, 2, 4, 5 count() + 1 daje 5 — a zadanie o id 5 już istnieje. Dwa zadania z tym samym id to przepis na kłopoty, gdy zaczniemy wskazywać zadania po id (np. „oznacz zadanie 5 jako wykonane” w zadaniu końcowym A). (Istnieje też gotowa funkcja max() — ale pętla pokazuje, co się naprawdę dzieje, i dla pustej listy działa bez dodatkowego if.)

Krok 3 — SAMODZIELNIE: strona dodaj-zadanie.php

Nowy plik projektu: dodaj-zadanie.php — w katalogu głównym taskquest/, obok index.php. Buduj go w trzech częściach i sprawdzaj po każdej.

3a — szkielet i formularz

Struktura pliku jest taka sama jak index.php (dane i logika na górze, prezentacja na dole):

  1. require_once: config/config.php, includes/funkcje_tekstowe.php (będzie potrzebna wyczyscTytul()), includes/funkcje_zadan.php, includes/funkcje_plikow.php,
  2. wartości początkowe: $tytulZadania, $opisZadania, $terminZadania, $xpZadania (puste teksty), $bledy (pusta tablica), $bladZapisu (null — tu trafi komunikat wyjątku, jak $bladZadan w index.php),
  3. $nazwaStrony = 'Dodaj zadanie — TaskQuest'; i dołączenie header.php,
  4. nagłówek <h2>Nowe zadanie</h2>, link powrotny do index.php,
  5. formularz: metoda POST, action="dodaj-zadanie.php", pola tytul (text, required, maxlength="100"), opis (textarea, maxlength="300"), termin (date, required), xp (number, min="1", max="100", required), przycisk „Dodaj zadanie”. Każde pole z <label for="..."> i wartością z odpowiedniej zmiennej,
  6. dołączenie footer.php.

Sprawdź: otwórz http://localhost/taskquest/dodaj-zadanie.php. Widać nagłówek TaskQuest, formularz i stopkę. Brak ostrzeżeń PHP. Wysłanie formularza na razie nic nie robi (strona po prostu się przeładuje).

3b — odbiór i sprawdzenie danych

Między wartościami początkowymi a prezentacją dodaj blok if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... }, który:

  1. odczytuje cztery pola przez ?? i od razu je czyści:
    • tytuł — istniejącą funkcją wyczyscTytul() (Lekcja 8: przycina i zamienia wielokrotne białe znaki na jedną spację),
    • opis — preg_replace('/\s+/', ' ', ...) i trim(): textarea pozwala wpisać kilka linii, a my chcemy mieć opis w jednej linii pliku CSV (pamiętasz ograniczenie numerów linii z Lekcji 12?),
    • termin i XP — trim(),
  2. wywołuje walidujNoweZadanie() i zapisuje wynik w $bledy.

W prezentacji, nad formularzem: gdy $bledy nie jest puste — ramka <div class="blad"> z tekstem „Popraw formularz:” i listą <ul> komunikatów.

Sprawdź (na czas sprawdzania dopisz do znacznika <form atrybut novalidate, żeby przeglądarka nie blokowała wysłania — potem go usuń): pusty formularz → trzy komunikaty; tytuł Nauka, termin pusty, XP 500 → dwa komunikaty, a w polu tytułu nadal jest Nauka.

3c — zapis do pliku

Gdy $bledy jest puste, zadanie trzeba dopisać do pliku. zapiszZadania() nadpisuje cały plik (tryb w), więc nie da się „dopisać jednej linii” — trzeba:

wczytajZadania()  →  $zadania[] = nowe zadanie  →  zapiszZadania()

Obie funkcje plikowe mogą rzucić wyjątek, więc cała ta trójka idzie do jednego try, a w catch zapamiętujemy komunikat w $bladZapisu. Nowe zadanie musi mieć dokładnie tę samą strukturę kluczy co pozostałe: id (z nastepneIdZadania()), tytul, opis, termin, wykonane (false — nowe zadanie nie jest wykonane), xp (jako int!).

Po udanym zapisie (na razie) ustaw zmienną $zadanieDodane = true; i wyświetl nad formularzem <p class="sukces">Zadanie zostało dodane.</p>. Gdy $bladZapisu !== null, wyświetl <p class="blad">Nie udało się dodać zadania: ...</p>.

Zastanów się, zanim napiszesz kod: co by się stało, gdyby wczytajZadania() była poza try, a przy awarii odczytu kod przyjąłby $zadania = [] i mimo to wywołał zapiszZadania()? Pomyśl o pliku, w którym jeden wiersz jest uszkodzony.

HINT

Wszystko, co ma się wykonać tylko po udanym odczycie, musi stać w try pod wywołaniem wczytajZadania(). Jeśli odczyt rzuci wyjątek, PHP przeskoczy do catch i zapis się w ogóle nie wykona. Pamiętaj o (int) przy XP — z formularza przychodzi tekst, a sumujXp() i porównania w aplikacji oczekują liczby.

POMOC
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST['tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST['opis'] ?? ''));
    // ... termin i xp ...

    $bledy = walidujNoweZadanie(/* ... */);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania[] = [
                'id' => /* ... */,
                // ... pozostałe klucze ...
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);
            // ... zadanie dodane ...
        } catch (Exception $wyjatek) {
            // ... komunikat do $bladZapisu ...
        }
    }
}

Formularz (fragment):

        <form method="post" action="dodaj-zadanie.php">
            <label for="tytul">Tytuł *</label>
            <input type="text" id="tytul" name="tytul" maxlength="100" required value="<?= $tytulZadania; ?>">

            <label for="opis">Opis</label>
            <textarea id="opis" name="opis" rows="3" maxlength="300"><?= $opisZadania; ?></textarea>

            <!-- termin, xp, przycisk -->
        </form>
ROZWIĄZANIE

(wersja przejściowa — w Kroku 4 zmienimy jedną rzecz)

<?php

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


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = [];
$bladZapisu = null;
$zadanieDodane = false;   // tylko w wersji przejściowej


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST['tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST['opis'] ?? ''));
    $terminZadania = trim($_POST['termin'] ?? '');
    $xpZadania = trim($_POST['xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania[] = [
                'id' => nastepneIdZadania($zadania),
                'tytul' => $tytulZadania,
                'opis' => $opisZadania,
                'termin' => $terminZadania,
                'wykonane' => false,
                'xp' => (int) $xpZadania
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);
            $zadanieDodane = true;   // tylko w wersji przejściowej
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek->getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Nowe zadanie</h2>
        <p><a href="index.php">← Wróć do listy zadań</a></p>

        <?php if ($zadanieDodane) { ?>
        <p class="sukces">Zadanie zostało dodane.</p>
        <?php } ?>

        <?php if (count($bledy) > 0) { ?>
        <div class="blad">
            <p>Popraw formularz:</p>
            <ul>
                <?php foreach ($bledy as $blad) { ?>
                <li><?= $blad; ?></li>
                <?php } ?>
            </ul>
        </div>
        <?php } ?>

        <?php if ($bladZapisu !== null) { ?>
        <p class="blad">Nie udało się dodać zadania: <?= $bladZapisu; ?></p>
        <?php } ?>

        <form method="post" action="dodaj-zadanie.php">
            <label for="tytul">Tytuł *</label>
            <input type="text" id="tytul" name="tytul" maxlength="100" required value="<?= $tytulZadania; ?>">

            <label for="opis">Opis</label>
            <textarea id="opis" name="opis" rows="3" maxlength="300"><?= $opisZadania; ?></textarea>

            <label for="termin">Termin *</label>
            <input type="date" id="termin" name="termin" required value="<?= $terminZadania; ?>">

            <label for="xp">Nagroda XP (1–100) *</label>
            <input type="number" id="xp" name="xp" min="1" max="100" required value="<?= $xpZadania; ?>">

            <button type="submit">Dodaj zadanie</button>
        </form>
<?php
require_once __DIR__ . '/includes/footer.php';

Klasa .sukces pojawi się w header.php w Kroku 4 — do tego czasu komunikat będzie po prostu zwykłym akapitem.

Odpowiedź na pytanie: przy uszkodzonym wierszu wczytajZadania() rzuca wyjątek. Gdyby kod mimo to zapisał listę złożoną tylko z nowego zadania, zapiszZadania() (tryb w!) skasowałaby wszystkie dotychczasowe zadania — zamiast jednego uszkodzonego wiersza Kama straciłaby cały plik. Dlatego: nie udało się wczytać → nie zapisujemy niczego.

Sprawdź: dodaj zadanie Powtórka z formularzy, termin za tydzień, 30 XP. Otwórz data/zadania.csv w edytorze — na końcu jest nowy wiersz z id 6 i wykonane = 0. Otwórz index.php — zadanie jest na liście, liczniki pokazują 6 zadań.

Krok 4 — SAMODZIELNIE: odświeżenie nie może dodać zadania drugi raz

Wróć na dodaj-zadanie.php, dodaj jeszcze jedno zadanie, a gdy zobaczysz „Zadanie zostało dodane.”, naciśnij F5 i potwierdź ponowne wysłanie. Otwórz data/zadania.csv: to samo zadanie jest dwa razy, z różnymi id. Przeglądarka po prostu powtórzyła ostatnie żądanie — a ostatnie żądanie to był POST z danymi.

Rozwiązanie to wzorzec POST → przekierowanie → GET (ang. Post/Redirect/Get, w skrócie PRG):

1. przeglądarka: POST dodaj-zadanie.php (dane)
2. PHP: zapisuje zadanie i odpowiada "idź pod adres index.php?dodano=1" (przekierowanie)
3. przeglądarka: GET index.php?dodano=1   ← to jest teraz "ostatnie żądanie"
4. F5 powtarza tylko krok 3 — czyli zwykłe wyświetlenie strony

Przekierowanie robi się tak:

header('Location: index.php?dodano=1');
exit;
  • header() wysyła przeglądarce nagłówek odpowiedzi — informację „techniczną”, która idzie przed treścią strony. Nagłówek Location mówi: „tu nic nie ma, idź pod ten adres”.
  • exit natychmiast kończy skrypt. Bez niego PHP wykonałby resztę pliku (wygenerowałby cały formularz), którego i tak nikt nie zobaczy.
  • Nagłówki muszą być wysłane przed jakąkolwiek treścią strony. U nas logika stoi nad require_once header.php, więc to jest spełnione. Gdyby wcześniej coś zostało wypisane, PHP może zgłosić Warning: Cannot modify header information - headers already sent i przekierowanie nie zadziała. (W XAMPP-ie włączone jest buforowanie wyjścia, więc przy małej ilości tekstu ostrzeżenie może się nie pojawić — ale nie polegaj na tym.)

Skąd index.php ma wiedzieć, że ma pokazać komunikat? Z adresu — dokładnie tak, jak w 3.2 PHP dostał ?imie=Kama. Parametr dodano=1 odczytamy przez $_GET.

Zadanie — zmień cztery pliki:

  1. dodaj-zadanie.php: w miejscu $zadanieDodane = true;(to usuń) wstaw przekierowanie na index.php?dodano=1 i exit. Usuń z tego pliku wszystko, co dotyczyło $zadanieDodane (wartość początkową i akapit „Zadanie zostało dodane.”) — komunikat przenosi się na stronę główną.
  2. index.php:
    • w bloku wywołań funkcji, po obliczeniu streaka, dodaj nowy blok // --- parametry z adresu strony (GET) --- i w nim zmienną $zadanieDodane — true tylko wtedy, gdy w adresie jest dodano=1,
    • pod <h2>Twoje zadania</h2> dodaj link <p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p> oraz — gdy $zadanieDodane — <p class="sukces">Zadanie zostało dodane.</p>. Oba elementy mają być przed konstrukcją if ($bladZadan !== null), żeby link działał także wtedy, gdy wczytanie zadań się nie udało.
  3. includes/header.php: dodaj regułę .sukces (zielona odmiana .blad) i trzy proste reguły dla formularza (etykieta w osobnej linii, pola na całą szerokość, odstęp przycisku).
  4. includes/footer.php: TaskQuest 1.3.
HINT

$_GET['dodano'] przy zwykłym wejściu na stronę nie istnieje — użyj ?? z pustym tekstem, a wynik porównaj przez === z tekstem '1' (pamiętasz? z adresu przychodzą teksty). Wynik porównania to od razu true albo false.

ROZWIĄZANIE

a) dodaj-zadanie.php — fragment w try:

            zapiszZadania(PLIK_ZADAN, $zadania);

            header('Location: index.php?dodano=1');
            exit;

b) index.php — nowy blok po $streak = aktualizujStreak(...);:

// --- parametry z adresu strony (GET) ---
$zadanieDodane = ($_GET['dodano'] ?? '') === '1';

c) index.php — prezentacja:

        <h2>Twoje zadania</h2>

        <p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>

        <?php if ($zadanieDodane) { ?>
        <p class="sukces">Zadanie zostało dodane.</p>
        <?php } ?>

        <?php if ($bladZadan !== null) { ?>

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

.sukces { border: 1px solid #080; background: #eaf7ea; color: #060; border-radius: 8px; padding: 8px 16px; }
form label { display: block; margin-top: 12px; }
form input, form textarea { width: 100%; padding: 6px; box-sizing: border-box; }
form button { margin-top: 16px; padding: 8px 16px; }

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

Sprawdź: dodaj zadanie. Przeglądarka sama przechodzi na index.php?dodano=1, widać zieloną ramkę i nowe zadanie. Naciśnij F5 — brak pytania o ponowne wysłanie, w pliku nie przybyło zadań. W F12 → Sieć zobaczysz dwa żądania: POST dodaj-zadanie.php ze statusem 302 (przekierowanie) i GET index.php?dodano=1 ze statusem 200.

Uczciwie o ograniczeniu: ramka „Zadanie zostało dodane.” pojawia się po każdym odświeżeniu index.php?dodano=1, a nawet po ręcznym wpisaniu takiego adresu — mimo że nic nie dodano. Adres nie jest dobrym miejscem na jednorazowe komunikaty. Porządnie rozwiążemy to w Lekcji 14 (sesje).

6. Kolejne zadanie — widoki listy przez GET

Formularz dodawania zmienia dane, więc używa POST. Teraz coś, co tylko wybiera, co pokazać — czyli zadanie dla GET.

SAMODZIELNIE — plik projektu: index.php. Nad listą zadań dodaj trzy linki: wszystkie, aktywne, wykonane, prowadzące do index.php?filtr=wszystkie, index.php?filtr=aktywne, index.php?filtr=wykonane. Lista ma pokazywać odpowiednio wszystkie zadania, tylko niewykonane albo tylko wykonane — zawsze posortowane wg terminu. Pod linkami wypisz Widok: <nazwa widoku>.

Wymagania:

  1. W bloku // --- parametry z adresu strony (GET) --- odczytaj parametr filtr do zmiennej $filtr. Brak parametru = wszystkie.
  2. Każda inna wartość niż aktywne czy wykonane (np. ?filtr=abc, ?filtr=AKTYWNE, ?filtr=) ma dawać widok wszystkie. Parametr z adresu to dane od użytkownika — nie ufamy mu.
  3. Do wyboru listy użyj match (znasz go z pobierzTytul()) i zapisz wynik w nowej zmiennej $zadaniaDoWyswietlenia. Brakuje posortowanej listy wykonanych — dodaj $wykonaneWgTerminu obok $aktywneWgTerminu.
  4. Pętla foreach wyświetlająca listę ma przechodzić po $zadaniaDoWyswietlenia zamiast po $zadaniaWgTerminu.
  5. Gdy w pliku są zadania, ale wybrany widok jest pusty (np. żadne nie jest wykonane) — wypisz Brak zadań w tym widoku.
  6. Bez zmian zostają: liczniki, XP do zdobycia, komunikat o awansie i „najpilniejsze zadanie” — one zawsze dotyczą całej listy, niezależnie od widoku.
HINT

Najpierw „zawęź” $filtr do trzech dozwolonych wartości (jeden if z && — bardzo podobny do sprawdzenia pola wykonane w sprawdzWierszZadania()), a dopiero potem użyj match. Dzięki temu match zawsze trafi na któryś ze swoich przypadków.

POMOC
// --- parametry z adresu strony (GET) ---
$filtr = $_GET['filtr'] ?? 'wszystkie';

if (/* ani aktywne, ani wykonane */) {
    $filtr = 'wszystkie';
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' => /* ... */,
    'aktywne' => /* ... */,
    'wykonane' => /* ... */
};

$zadanieDodane = ($_GET['dodano'] ?? '') === '1';
ROZWIĄZANIE

a) index.php — lista zadań, pod $aktywneWgTerminu = ...:

$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);

b) index.php — cały blok parametrów GET:

// --- parametry z adresu strony (GET) ---
$filtr = $_GET['filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' => $zadaniaWgTerminu,
    'aktywne' => $aktywneWgTerminu,
    'wykonane' => $wykonaneWgTerminu
};

$zadanieDodane = ($_GET['dodano'] ?? '') === '1';

c) index.php — w prezentacji zamień początek listy (dotąd: echo "<h3>Lista zadań (wg terminu)</h3>"; i foreach ($zadaniaWgTerminu as $zadanie) {) na:

        echo "<h3>Lista zadań (wg terminu)</h3>";
        echo "<p>Pokaż: <a href=\"index.php?filtr=wszystkie\">wszystkie</a> | "
            . "<a href=\"index.php?filtr=aktywne\">aktywne</a> | "
            . "<a href=\"index.php?filtr=wykonane\">wykonane</a></p>";
        echo "<p>Widok: <strong>$filtr</strong></p>";

        if ($liczbaWszystkich > 0 && count($zadaniaDoWyswietlenia) === 0) {
            echo "<p>Brak zadań w tym widoku.</p>";
        }

        foreach ($zadaniaDoWyswietlenia as $zadanie) {

Reszta pętli bez zmian.

Zwróć uwagę: $filtr trafia na stronę (Widok: ...), a mimo to jest bezpieczny — po sprawdzeniu może mieć tylko jedną z trzech znanych nam wartości, niezależnie od tego, co ktoś wpisze w adres. Takie „zawężanie do listy dozwolonych wartości” to jedna z najskuteczniejszych technik obrony przed złymi danymi.

Ostatnia drobna zmiana: diagnostyka.php — dopisz 'dodaj-zadanie.php' do tablicy $wymaganePliki. Strona powinna pokazać 12 × [OK]. (Lista plików .php w config/ i includes/ się nie zmienia — nowy plik leży w katalogu głównym.)

7. Testy

Test 1 — walidujNoweZadanie() na granicach

BRUDNOPIS. Przewidź wynik każdego przypadku, zanim uruchomisz kod:

<?php

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

$przypadki = [
    ['Nauka PHP', '', '2026-10-02', '20'],
    ['', '', '2026-10-02', '20'],
    [str_repeat('a', 101), '', '2026-10-02', '20'],
    [str_repeat('ą', 100), '', '2026-10-02', '20'],
    ['Nauka PHP', str_repeat('x', 301), '2026-10-02', '20'],
    ['Nauka PHP', '', '', '20'],
    ['Nauka PHP', '', '02.10.2026', '20'],
    ['Nauka PHP', '', '2026-02-29', '20'],
    ['Nauka PHP', '', '2028-02-29', '20'],
    ['Nauka PHP', '', '2026-13-01', '20'],
    ['Nauka PHP', '', '2026-10-02', '0'],
    ['Nauka PHP', '', '2026-10-02', '1'],
    ['Nauka PHP', '', '2026-10-02', '100'],
    ['Nauka PHP', '', '2026-10-02', '101'],
    ['Nauka PHP', '', '2026-10-02', 'dużo'],
    ['Nauka PHP', '', '2026-10-02', '12abc'],
    ['Nauka PHP', '', '2026-10-02', '-5'],
    ['Nauka PHP', '', '2026-10-02', '7.5'],
    ['', '', '', ''],
];

foreach ($przypadki as $numer => $dane) {
    $bledy = walidujNoweZadanie($dane[0], $dane[1], $dane[2], $dane[3]);
    echo ($numer + 1) . ". " . (count($bledy) === 0 ? "OK" : implode(' | ', $bledy)) . "<br>";
}
NrDane (co jest nietypowe)Oczekiwany wynik
1wszystko poprawneOK
2pusty tytułTytuł jest wymagany.
3tytuł: 101 znakówTytuł może mieć najwyżej 100 znaków.
4tytuł: 100 × ąOK (100 znaków, mimo 200 bajtów — dlatego mb_strlen())
5opis: 301 znakówOpis może mieć najwyżej 300 znaków.
6pusty terminTermin musi być datą w formacie RRRR-MM-DD.
7termin 02.10.2026Termin musi być datą w formacie RRRR-MM-DD.
8termin 2026-02-29Taki dzień nie istnieje w kalendarzu. (2026 nie jest przestępny)
9termin 2028-02-29OK (2028 jest przestępny)
10termin 2026-13-01Taki dzień nie istnieje w kalendarzu.
11XP 0XP musi być z zakresu 1–100.
12XP 1OK
13XP 100OK
14XP 101XP musi być z zakresu 1–100.
15XP dużoXP musi być liczbą całkowitą.
16XP 12abcXP musi być liczbą całkowitą.
17XP -5XP musi być liczbą całkowitą. (minus to nie cyfra)
18XP 7.5XP musi być liczbą całkowitą.
19wszystko pusteTytuł jest wymagany. \| Termin musi być datą w formacie RRRR-MM-DD. \| XP musi być liczbą całkowitą.

implode(' | ', $bledy) skleja elementy tablicy w jeden tekst, wstawiając między nie ' | ' — tylko na potrzeby testu.

Test 2 — nastepneIdZadania()

BRUDNOPIS.

Lista idOczekiwany wynik
(pusta lista)1
1, 2, 34
3, 10, 711
1, 2, 4, 56 (a nie 5!)
prawdziwe zadania: nastepneIdZadania(wczytajZadania(PLIK_ZADAN)) na oryginalnym pliku6

(Do ostatniego przypadku dołącz w brudnopisie także config/config.php i includes/funkcje_plikow.php.)

Test 3 — dodawanie przez stronę

PLIK PROJEKTU dodaj-zadanie.php. Na czas tego testu dopisz do znacznika <form atrybut novalidate (przeglądarka nie będzie blokować wysłania), a po teście usuń go. Przed testem przywróć oryginalny data/zadania.csv z kopii.

Co wpisujeszOczekiwany efekt
nic (pusty formularz)trzy komunikaty w czerwonej ramce, plik bez zmian
tytuł Nauka formularzy , termin pusty, XP 30jeden komunikat (termin); w polu tytułu jest już Nauka formularzy (oczyszczony!), XP nadal 30
tytuł Nauka formularzy, poprawny termin, XP 30przejście na index.php?dodano=1, zielona ramka, zadanie na liście; w pliku nowy wiersz z id 6
tytuł Zadanie; z "cudzysłowem", opis w dwóch liniach, poprawny termin, XP 5zadanie dodane z id 7; w pliku tytuł jest w cudzysłowie, a cudzysłowy w środku są podwojone ("") — tak fputcsv() chroni średniki i cudzysłowy; opis w jednej linii; strona główna nadal działa
F5 na index.php?dodano=1brak pytania o ponowne wysłanie, w pliku nadal 7 zadań
to samo zadanie jeszcze raz przez formularzdodaje się jako id 8 — aplikacja nie sprawdza duplikatów (tego nie wymagaliśmy)

Test 4 — dodawanie, gdy plik ma problem

PLIK DANYCH PROJEKTU. Po każdym wierszu przywróć oryginalny data/zadania.csv.

ZmianaOczekiwany efekt po wysłaniu poprawnego formularza
plik nazywa się zadania_x.csvczerwona ramka: Nie udało się dodać zadania: Plik z zadaniami nie istnieje.; formularz zachowuje wpisane dane; w data/ nie powstał nowy plik
w wierszu zadania 3 pierwszy średnik zamieniony na spacjęramka: ... Linia 4: oczekiwano 6 pól, a jest 5.; plik jest dokładnie taki jak przed wysłaniem (nic nie skasowano, nie dopisano)
plik oznaczony jako tylko do odczytu (Windows: prawy przycisk → Właściwości → „Tylko do odczytu”)ostrzeżenie PHP Warning: fopen(...): Failed to open stream: Permission denied oraz ramka: Nie udało się dodać zadania: Nie udało się zapisać pliku z zadaniami. (po teście odznacz „Tylko do odczytu”)
w pliku tylko nagłówekzadanie dodane z id 1

W drugim wierszu tabeli widać, po co odczyt i zapis są w jednym try.

Test 5 — widoki listy (GET)

PLIK PROJEKTU index.php. Na oryginalnym pliku z 5 zadaniami (2 wykonane, 3 aktywne):

AdresOczekiwany efekt
index.phpWidok: wszystkie, 5 zadań
index.php?filtr=aktywneWidok: aktywne, 3 zadania, wszystkie z [ ], posortowane wg terminu
index.php?filtr=wykonaneWidok: wykonane, 2 zadania z [X]
index.php?filtr=abcWidok: wszystkie, 5 zadań, brak błędów
index.php?filtr=AKTYWNEWidok: wszystkie (wielkość liter ma znaczenie)
index.php?filtr=aktywne&dodano=1widok aktywnych i zielona ramka
index.php?filtr=wykonane, gdy w pliku wszystkie zadania mają wykonane = 0Brak zadań w tym widoku.

W każdym widoku liczniki (Wszystkie: 5 | Wykonane: 2 | Aktywne: 3) i „najpilniejsze zadanie” wyglądają tak samo.

Test 6 — regresja

Uruchom obok siebie taskquest_lekcja12/index.php i nowy taskquest/index.php tego samego dnia (na oryginalnym pliku zadań). Różnice mogą być tylko cztery: link „+ Dodaj zadanie”, linia z linkami widoków, linia Widok: wszystkie i wersja w stopce (1.2 → 1.3). Liczniki, pasek, komunikaty terminów i lista — identyczne. diagnostyka.php pokazuje 12 × [OK]. Test awarii z Lekcji 12 (zmiana nazwy pliku z zadaniami) nadal daje czerwoną ramkę na stronie głównej — a link „+ Dodaj zadanie” jest nad nią.

Sprzątanie

Gdy testy przeszły:

  1. usuń brudnopis.php,
  2. usuń atrybut novalidate z formularza w dodaj-zadanie.php, jeśli został,
  3. przywróć data/zadania.csv z kopii (5 oryginalnych zadań) — albo zostaw zadania, które naprawdę chcesz mieć w TaskQuest,
  4. odśwież index.php i diagnostyka.php.

8. Obowiązkowe eksperymenty

Gdzie: wszystkie eksperymenty wykonuj w plikach projektu wskazanych przy każdym z nich. Po każdym przywróć kod do stanu z sekcji 5–6 i oryginalny data/zadania.csv.

Eksperyment 1: przeglądarka to nie strażnik

Plik projektu dodaj-zadanie.php. Bez novalidate spróbuj wysłać pusty formularz — przeglądarka nie pozwala. Teraz otwórz F12 → Elementy, kliknij pole XP i usuń z niego atrybuty min, max i required (dwuklik na atrybucie → usuń tekst). Wpisz 999 i wyślij. Kto zatrzymał złe dane? Co by się stało, gdyby w PHP nie było walidujNoweZadanie()? Sprawdź też, co zrobi strona, gdy w F12 zmienisz type="number" na type="text" i wpiszesz dużo.

Eksperyment 2: formularz wysłany metodą GET

Plik projektu dodaj-zadanie.php. Zmień method="post" na method="get" i dodaj poprawne zadanie. Co widać w pasku adresu? Czy zadanie zostało dodane? Dlaczego nie — który warunek w kodzie nie jest spełniony? Następnie zastanów się: gdyby kod sprawdzał $_GET zamiast $_POST, to czym groziłoby wysłanie komuś linku z takim adresem albo zapisanie go w zakładkach?

Eksperyment 3: match bez listy dozwolonych wartości

Plik projektu index.php. Zakomentuj if, który zamienia nieznany filtr na wszystkie, i otwórz index.php?filtr=abc. Przeczytaj pierwszą linię komunikatu: Fatal error: Uncaught UnhandledMatchError .... Czy to Exception, czy Error (Lekcja 12, sekcja 3.8)? Dlaczego nie ratuje nas żaden try? Dopisz do match przypadek default => $zadaniaWgTerminu i sprawdź jeszcze raz — działa, ale co teraz wypisze linia Widok: ... dla adresu ?filtr=abc? Które rozwiązanie jest lepsze i dlaczego?

Eksperyment 4: (int) zamiast regexa

Plik projektu includes/funkcje_zadan.php. W walidujNoweZadanie() zamień sprawdzenie XP na samo:

    if ((int) $xpZadania < 1 || (int) $xpZadania > 100) {
        $bledy[] = "XP musi być z zakresu 1–100.";
    }

Uruchom Test 1. Które przypadki zmieniły wynik? Co zostanie zapisane w pliku, jeśli ktoś wyśle 12abc? Jaki komunikat dostanie ktoś, kto wpisał dużo — i czy mówi mu prawdę?

Eksperyment 5: tekst użytkownika trafia na stronę (zapowiedź Lekcji 22)

Plik projektu dodaj-zadanie.php (z novalidate).

a) Wpisz tytuł Zadanie "PHP", termin zostaw pusty i wyślij. Formularz wraca z błędem — co jest teraz w polu tytułu? Otwórz F12 → Elementy i spójrz na atrybut value. Dlaczego tekst urwał się na cudzysłowie?

b) Bez novalidate: dodaj poprawne zadanie o tytule <b>Bardzo</b> ważne. Otwórz stronę główną. Jak wygląda tytuł na liście? Przeglądarka potraktowała tekst od użytkownika jak kod HTML. Pogrubienie jest niewinne — ale w to samo miejsce ktoś mógłby wpisać znaczniki, które zmieniają wygląd całej strony albo uruchamiają skrypt. To się nazywa wstrzykiwanie HTML (a w wersji ze skryptem — XSS) i naprawimy to funkcją htmlspecialchars() w Lekcji 22.

Po eksperymencie usuń to zadanie z pliku (przywróć kopię).

Eksperyment 6 (dodatkowy): po co exit?

Plik projektu dodaj-zadanie.php. Usuń exit; spod header(...), a pod header(...) dopisz tymczasowo file_put_contents(__DIR__ . '/po_przekierowaniu.txt', 'wykonano');. Dodaj zadanie. Przekierowanie działa — a czy plik po_przekierowaniu.txt powstał? Co to mówi o tym, co robi header(), a czego nie robi? Przywróć exit; i usuń dopisaną linię oraz plik.

9. ROZWIĄZANIE

ROZWIĄZANIE

Pełny kod TaskQuest po Lekcji 13. Nowe lub zmienione: dodaj-zadanie.php (nowy), index.php, includes/funkcje_zadan.php, includes/header.php, includes/footer.php, diagnostyka.php. Bez zmian względem Lekcji 12: config/config.php, includes/funkcje_grywalizacji.php, includes/funkcje_tekstowe.php, includes/funkcje_terminow.php, includes/funkcje_diagnostyczne.php, includes/funkcje_plikow.php, data/zadania.csv (poza zadaniami dodanymi przez formularz).

includes/funkcje_zadan.php

Cztery dotychczasowe funkcje (pobierzWykonaneZadania(), pobierzAktywneZadania(), sumujXp(), sortujPoTerminie()) zostają bez zmian. Na końcu pliku dochodzą:

function nastepneIdZadania(array $zadania): int
{
    $najwiekszeId = 0;

    foreach ($zadania as $zadanie) {
        if ($zadanie['id'] > $najwiekszeId) {
            $najwiekszeId = $zadanie['id'];
        }
    }

    return $najwiekszeId + 1;
}


function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array
{
    $bledy = [];

    if ($tytulZadania === '') {
        $bledy[] = "Tytuł jest wymagany.";
    } elseif (mb_strlen($tytulZadania) > 100) {
        $bledy[] = "Tytuł może mieć najwyżej 100 znaków.";
    }

    if (mb_strlen($opisZadania) > 300) {
        $bledy[] = "Opis może mieć najwyżej 300 znaków.";
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $terminZadania)) {
        $bledy[] = "Termin musi być datą w formacie RRRR-MM-DD.";
    } else {
        $czesciDaty = explode('-', $terminZadania);

        if (!checkdate((int) $czesciDaty[1], (int) $czesciDaty[2], (int) $czesciDaty[0])) {
            $bledy[] = "Taki dzień nie istnieje w kalendarzu.";
        }
    }

    if (!preg_match('/^\d+$/', $xpZadania)) {
        $bledy[] = "XP musi być liczbą całkowitą.";
    } elseif ((int) $xpZadania < 1 || (int) $xpZadania > 100) {
        $bledy[] = "XP musi być z zakresu 1–100.";
    }

    return $bledy;
}

dodaj-zadanie.php

<?php

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


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = [];
$bladZapisu = null;


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST['tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST['opis'] ?? ''));
    $terminZadania = trim($_POST['termin'] ?? '');
    $xpZadania = trim($_POST['xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania[] = [
                'id' => nastepneIdZadania($zadania),
                'tytul' => $tytulZadania,
                'opis' => $opisZadania,
                'termin' => $terminZadania,
                'wykonane' => false,
                'xp' => (int) $xpZadania
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);

            header('Location: index.php?dodano=1');
            exit;
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek->getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Nowe zadanie</h2>
        <p><a href="index.php">← Wróć do listy zadań</a></p>

        <?php if (count($bledy) > 0) { ?>
        <div class="blad">
            <p>Popraw formularz:</p>
            <ul>
                <?php foreach ($bledy as $blad) { ?>
                <li><?= $blad; ?></li>
                <?php } ?>
            </ul>
        </div>
        <?php } ?>

        <?php if ($bladZapisu !== null) { ?>
        <p class="blad">Nie udało się dodać zadania: <?= $bladZapisu; ?></p>
        <?php } ?>

        <form method="post" action="dodaj-zadanie.php">
            <label for="tytul">Tytuł *</label>
            <input type="text" id="tytul" name="tytul" maxlength="100" required value="<?= $tytulZadania; ?>">

            <label for="opis">Opis</label>
            <textarea id="opis" name="opis" rows="3" maxlength="300"><?= $opisZadania; ?></textarea>

            <label for="termin">Termin *</label>
            <input type="date" id="termin" name="termin" required value="<?= $terminZadania; ?>">

            <label for="xp">Nagroda XP (1–100) *</label>
            <input type="number" id="xp" name="xp" min="1" max="100" required value="<?= $xpZadania; ?>">

            <button type="submit">Dodaj zadanie</button>
        </form>
<?php
require_once __DIR__ . '/includes/footer.php';

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);
$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);

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

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


// --- parametry z adresu strony (GET) ---
$filtr = $_GET['filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' && $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' => $zadaniaWgTerminu,
    'aktywne' => $aktywneWgTerminu,
    'wykonane' => $wykonaneWgTerminu
};

$zadanieDodane = ($_GET['dodano'] ?? '') === '1';


// --- 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>

        <p><a href="dodaj-zadanie.php">+ Dodaj zadanie</a></p>

        <?php if ($zadanieDodane) { ?>
        <p class="sukces">Zadanie zostało dodane.</p>
        <?php } ?>

        <?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>";
        echo "<p>Pokaż: <a href=\"index.php?filtr=wszystkie\">wszystkie</a> | "
            . "<a href=\"index.php?filtr=aktywne\">aktywne</a> | "
            . "<a href=\"index.php?filtr=wykonane\">wykonane</a></p>";
        echo "<p>Widok: <strong>$filtr</strong></p>";

        if ($liczbaWszystkich > 0 && count($zadaniaDoWyswietlenia) === 0) {
            echo "<p>Brak zadań w tym widoku.</p>";
        }

        foreach ($zadaniaDoWyswietlenia 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; }
        .sukces { border: 1px solid #080; background: #eaf7ea; color: #060; border-radius: 8px; padding: 8px 16px; }
        form label { display: block; margin-top: 12px; }
        form input, form textarea { width: 100%; padding: 6px; box-sizing: border-box; }
        form button { margin-top: 16px; padding: 8px 16px; }
    </style>
</head>
<body>
    <div class="panel">
        <h1>TaskQuest</h1>

includes/footer.php

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

diagnostyka.php

Jedyna zmiana: w tablicy $wymaganePliki dochodzi pozycja 'dodaj-zadanie.php' (razem 12 ścieżek). Reszta pliku bez zmian względem Lekcji 11.

10. Co zmieniliśmy w TaskQuest?

Zostaje

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

wszystkie funkcje z Lekcji 1–12 — kod bez zmian
struktura tablicy $zadania (klucze: id, tytul, opis, termin, wykonane, xp)
format data/zadania.csv
obsługa awarii wczytania zadań w index.php (try/catch, ramka .blad)

Dochodzi

// index.php
$wykonaneWgTerminu        // sortujPoTerminie($wykonaneZadania)
$filtr                    // widok listy z $_GET['filtr']: 'wszystkie' | 'aktywne' | 'wykonane' (inne wartości → 'wszystkie')
$zadaniaDoWyswietlenia    // lista wybrana przez match ($filtr)
$zadanieDodane            // bool: czy w adresie jest dodano=1

// dodaj-zadanie.php (nowy plik)
$tytulZadania, $opisZadania, $terminZadania, $xpZadania   // dane nowego zadania z formularza (teksty, oczyszczone)
$bledy                    // tablica komunikatów z walidujNoweZadanie()
$bladZapisu               // null albo komunikat wyjątku z wczytajZadania()/zapiszZadania()

// includes/funkcje_zadan.php
nastepneIdZadania()       // największe id + 1 (dla pustej listy 1)
walidujNoweZadanie()      // zwraca tablicę komunikatów o błędach (pusta = dane poprawne)

// includes/header.php
.sukces, form label, form input, form textarea, form button

Nowe narzędzia PHP i HTML: <form method action>, <input type="text|date|number">, <textarea>, <label for>, atrybuty name, required, min, max, maxlength, novalidate; $_GET, $_POST, $_SERVER['REQUEST_METHOD'], operator ??, checkdate(), header('Location: ...') + exit, wzorzec POST → przekierowanie → GET, match na liście dozwolonych wartości.

Zmieniamy

  • index.php: link „+ Dodaj zadanie” i komunikat o dodaniu pod <h2>Twoje zadania</h2>; nowy blok „parametry z adresu strony (GET)”; pętla listy przechodzi po $zadaniaDoWyswietlenia zamiast po $zadaniaWgTerminu; nad listą linki widoków, linia Widok: ... i komunikat o pustym widoku,
  • includes/header.php: nowe reguły CSS,
  • includes/footer.php: TaskQuest 1.3,
  • diagnostyka.php: 12 wymaganych plików,
  • zapiszZadania() — kod bez zmian, ale pierwszy raz jest wywoływana ze strony aplikacji (w try / catch, razem z wczytajZadania()).

Usuwamy

Nic. Żadna zmienna ani funkcja z Lekcji 12 nie została usunięta. $zadaniaWgTerminu nadal istnieje — jest jedną z list, spośród których wybiera match.

Co nowego potrafi TaskQuest po tej lekcji?

Po Lekcji 13 TaskQuest przyjmuje zadania od użytkownika. Na stronie dodaj-zadanie.php jest formularz, którego dane PHP sprawdza sam (nie ufając przeglądarce), pokazuje wszystkie błędy naraz i nie każe wpisywać wszystkiego od nowa. Poprawne zadanie dostaje unikalne id i jest dopisywane do data/zadania.csv — ale tylko wtedy, gdy plik udało się poprawnie wczytać, więc uszkodzony plik nie zostanie nadpisany. Po zapisie aplikacja przekierowuje na stronę główną, dzięki czemu odświeżenie nie dodaje zadania drugi raz. Lista zadań ma trzy widoki (wszystkie, aktywne, wykonane) wybierane przez adres strony.

Do samodzielnego sprawdzenia

[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Potrafię wyjaśnić, czym różni się żądanie GET od POST i gdzie jadą dane w każdym z nich.
[ ] Wiem, że atrybut name pola staje się kluczem w $_GET / $_POST, a wartości zawsze są tekstami.
[ ] Potrafię odróżnić pierwsze wejście od wysłania formularza ($_SERVER['REQUEST_METHOD']).
[ ] Używam ??, żeby nie sięgać po nieistniejące klucze.
[ ] Wiem, dlaczego required/min/max nie zastępują sprawdzania w PHP.
[ ] Wiem, dlaczego błędy w formularzu to tablica komunikatów, a awaria pliku — wyjątek.
[ ] Mam walidujNoweZadanie() i nastepneIdZadania() w funkcje_zadan.php; Test 1 i Test 2 się zgadzają.
[ ] dodaj-zadanie.php zachowuje wpisane dane po błędzie i pokazuje wszystkie błędy naraz.
[ ] Odczyt i zapis są w jednym try — uszkodzony plik nie zostaje nadpisany (Test 4).
[ ] Po dodaniu zadania następuje przekierowanie; F5 nie dodaje zadania drugi raz.
[ ] index.php ma trzy widoki listy; nieznany filtr daje widok "wszystkie" bez błędów (Test 5).
[ ] diagnostyka.php pokazuje 12 razy [OK]; strona główna jak po Lekcji 12 (Test 6).
[ ] Wiem, jaki problem pokazał Eksperyment 5 i w której lekcji go naprawimy.
[ ] Usunęłam/usunąłem brudnopis.php i novalidate; plik zadań ma zawartość, którą chcę zachować.

Pytania kontrolne

  1. Wpisujesz w formularzu (metoda GET) Kama w pole z name="imie" i id="pole1". Jak wygląda adres po wysłaniu i jaki klucz pojawi się w $_GET? Co się zmieni, gdy usuniesz z pola atrybut name?

  2. Przewidź wynik każdej linii, gdy strona została otwarta zwykłym wejściem (bez formularza i bez niczego w adresie):echo $_SERVER['REQUEST_METHOD'];
    echo $_POST['tytul'] ?? 'brak';
    echo ($_GET['dodano'] ?? '') === '1' ? 'tak' : 'nie';
    var_dump($_POST);
    Pole xp ma type="number" i min="1". Podaj dwa sposoby, w jakie użytkownik może mimo to wysłać xp=-50. Która część TaskQuest go zatrzyma?

  3. Dlaczego sprawdzamy XP regexem /^\d+$/ przed porównaniem z zakresem? Co zapisałby TaskQuest dla 12abc, a jaki komunikat zobaczyłby użytkownik dla dużo, gdyby regexa nie było?

  4. Dlaczego walidujNoweZadanie() zwraca tablicę komunikatów, a nie rzuca wyjątku przy pierwszym błędzie — skoro w sprawdzWierszZadania() wyjątek był dobrym pomysłem?

  5. W dodaj-zadanie.php wywołania wczytajZadania() i zapiszZadania() są w jednym bloku try. Opisz krok po kroku, co stałoby się z plikiem zawierającym jeden uszkodzony wiersz, gdyby przy awarii odczytu kod przyjął pustą listę i mimo to zapisał nowe zadanie.

  6. Dlaczego nastepneIdZadania() szuka największego id, zamiast liczyć count($zadania) + 1? Podaj przykład listy, dla której te dwa sposoby dają różny wynik.

  7. Co się dzieje po kliknięciu „Dodaj zadanie” — wymień po kolei żądania, które widać w F12 → Sieć, z ich metodami i statusami. Dlaczego po tym F5 nie dodaje zadania ponownie?

  8. Dlaczego filtr listy zadań przekazujemy przez GET, a dodawanie zadania przez POST? Co mogłoby się stać, gdyby dodawanie działało przez link w stylu dodaj-zadanie.php?tytul=...&xp=...?

  9. W index.php wartość $filtr pochodzi od użytkownika, a mimo to wypisujemy ją na stronie bez obaw. Dlaczego jest bezpieczna? Czy to samo można powiedzieć o tytule zadania dodanym przez formularz (Eksperyment 5)?

  10. Komunikat „Zadanie zostało dodane.” można wywołać, wpisując ręcznie index.php?dodano=1. Dlaczego? Jakiego mechanizmu brakuje, żeby komunikat był naprawdę jednorazowy?

Zadanie końcowe — rozbuduj TaskQuest samodzielnie

Wybierz jedno (albo oba):

A) „Zrobione!” — oznaczanie zadania jako wykonanego. Przy każdym niewykonanym zadaniu na liście w index.php dodaj mały formularz z jednym przyciskiem „Zrobione” (metoda POST, action="oznacz-wykonane.php") i ukrytym polem <input type="hidden" name="id" value="..."> z id zadania. Utwórz stronę oznacz-wykonane.php, która:

  1. przyjmuje tylko żądania POST (dla GET od razu przekierowuje na index.php),
  2. sprawdza, czy id składa się z samych cyfr,
  3. w try: wczytuje zadania, znajduje zadanie o tym id (pętla foreach z kluczem: foreach ($zadania as $klucz => $zadanie)), ustawia mu 'wykonane' => true przez $zadania[$klucz]['wykonane'] = true;, zapisuje plik i przekierowuje na index.php?wykonano=1,
  4. gdy zadania o takim id nie ma albo wystąpi wyjątek — pokazuje czytelny komunikat i link powrotny.

Na stronie głównej dodaj komunikat „Zadanie oznaczone jako wykonane.” (analogicznie do dodano=1). Pytania do przemyślenia: dlaczego przycisk to formularz POST, a nie link oznacz-wykonane.php?id=3? Czy ukryte pole jest naprawdę ukryte przed użytkownikiem (sprawdź w F12)? Co się stanie, gdy ktoś wyśle id nieistniejącego zadania? Nie zmieniaj $xp użytkownika — XP jest nadal symulowane, a przyznawanie nagrody wróci razem z bazą danych.

B) Wyszukiwarka. Nad listą zadań w index.php dodaj formularz wyszukiwania (metoda GET, pole name="szukaj"). Lista ma pokazywać tylko te zadania z bieżącego widoku, których oczyszczony tytuł zawiera wpisane słowo — bez względu na wielkość liter (stripos() znasz z Lekcji 8). Wymagania: puste pole = brak filtrowania; po wyszukaniu słowo zostaje w polu; wyszukiwanie ma działać razem z widokiem (np. aktywne + „PHP”) — podpowiedź: ukryte pole <input type="hidden" name="filtr" value="..."> w formularzu wyszukiwania. Dlaczego tu GET jest lepszy niż POST? Sprawdź: czy adres z wynikami wyszukiwania można zapisać w zakładkach? Uwaga: wpisane słowo wraca na stronę (w polu i ewentualnie w komunikacie) — pamiętaj o Eksperymencie 5.

Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory.

Most do Lekcji 14

TaskQuest przyjmuje już dane od użytkownika. Ale każda strona nadal działa „w próżni”: skrypt kończy się po wysłaniu odpowiedzi i niczego nie pamięta. Stąd nasze obejścia:

"Zadanie zostało dodane."   →  przekazane w adresie (?dodano=1) — widać je po każdym odświeżeniu i każdy może je "podrobić"
$imieUzytkownika, $login     →  wpisane na sztywno w index.php — TaskQuest nie wie, KTO z niego korzysta
wybrany widok listy          →  znika, gdy wejdziesz na stronę bez ?filtr=...

Potrzebujemy sposobu, żeby aplikacja pamiętała coś między kolejnymi żądaniami tego samego użytkownika — jego login, jednorazowy komunikat, ulubiony widok. To temat Lekcji 14: cookies i sesje.


PODSUMOWANIE LEKCJI

W skrócie

Jak to w ogóle działa

  • Każde otwarcie strony to żądanie → PHP startuje od zera → odsyła odpowiedź → skrypt się kończy i wszystkie zmienne znikają. Dlatego dane trzeba przesyłać przy każdym żądaniu (w adresie albo w treści żądania).
  • GET — dane w adresie, po ?, w parach klucz=wartość rozdzielonych &. Trafiają do $_GET.
  • POST — dane w treści żądania, adres się nie zmienia. Trafiają do $_POST.
  • Zasada na cały kurs: GET — gdy tylko czytamy. POST — gdy coś zmieniamy.

Co wędruje z formularza do PHP

  • Kluczem w $_GET / $_POST jest atrybut name, nie id. Pole bez name nie zostanie wysłane w ogóle.
  • Wszystko, co przychodzi, jest tekstem — także z type="number" i type="date" (data zawsze w formacie RRRR-MM-DD, niezależnie od tego, jak przeglądarka ją wyświetla).
  • Pole niezaznaczone (checkbox, radio) nie wysyła nic — klucza po prostu nie ma w tablicy. Dlatego sprawdzamy isset() lub ??, zamiast porównywać wartość.
  • Żeby dotarł więcej niż jeden wybór, nazwa musi mieć nawiasy: name="food[]", <select name="hobby[]" multiple>. W PHP powstaje wtedy tablica.
  • Przycisk trafia do $_POST tylko wtedy, gdy ma name (<button type="submit" name="przycisk"> → $_POST['przycisk']).

Odbiór danych w PHP

  • $_SERVER['REQUEST_METHOD'] odróżnia pierwsze wejście na stronę ('GET') od wysłania formularza ('POST'). Cała obsługa formularza siedzi w if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... }.
  • ?? chroni przed Warning: Undefined array key: $tytul = $_POST['tytul'] ?? '';.
  • Uwaga na pułapkę: ?? reaguje tylko na brak klucza albo null. Pusty tekst z formularza istnieje, więc $_POST['xp'] ?? 10 dla pustego pola da '', a nie 10.

Sprawdzanie danych

  • required, min, max, maxlength to wygoda dla użytkownika, nie ochrona: można dopisać novalidate, usunąć atrybut w F12 albo wysłać dane bez przeglądarki. PHP zawsze sprawdza dane sam.
  • Błąd użytkownika (pusty tytuł, XP = 500) → tablica komunikatów $bledy, pokazana w całości. Awaria (nie ma pliku, nie da się zapisać) → wyjątek i try / catch.
  • Rzutowanie (int) nie jest walidacją: (int) "12abc" daje 12, (int) "dużo" daje 0. Najpierw sprawdzamy format (np. preg_match('/^\d+$/', $xp)), dopiero potem zakres.

Po zapisie

  • Odświeżenie strony powtarza ostatnie żądanie. Jeśli był to POST — zadanie dopisze się drugi raz. Ratunek to POST → przekierowanie → GET: header('Location: index.php?dodano=1'); exit;.
  • header() musi zostać wywołane przed wypisaniem czegokolwiek na stronę, a exit jest konieczne — header() sam nie przerywa skryptu.
  • Komunikat przekazany w adresie (?dodano=1) każdy może wpisać ręcznie. To obejście do czasu poznania sesji (Lekcja 14).
  • POST nie jest tajny — dane widać w F12 → Sieć → Ładunek. Szyfruje HTTPS, nie metoda.

Co trzeba umieć na INF.03?

  • napisać formularz „wysyłający dane metodą bezpieczną do tego samego pliku” — w języku arkuszy CKE „metoda bezpieczna” to POST (dane nie pokazują się w adresie). Uwaga: to nie to samo co „szyfrowana” — o tym była mowa w lekcji;
  • wysłać formularz do tego samego pliku — czyli pominąć atrybut action albo wpisać w nim nazwę bieżącego pliku;
  • zbudować pola z etykietami połączonymi z polem (<label for="..."> + id w polu), w tym pole edycyjne typu numerycznego (<input type="number">) i listę rozwijaną (<select>), której opcje są generowane skryptem PHP z danych z bazy;
  • napisać odnośnik przesyłający parametr metodą GET do tej samej strony (index.php?idKontynent=6) i odczytać ten parametr w PHP;
  • przyjąć wartość domyślną, gdy parametru brak — arkusz wprost tego wymaga („a w przypadku braku – wartością 6”). Arkusz podaje w tabeli funkcji isset($zmienna); w naszym kursie krótszym zapisem jest ??;
  • uruchomić fragment skryptu tylko wtedy, gdy formularz został wysłany („działa tylko w przypadku, gdy z formularza zostały przesłane dane”) — isset($_POST['...']) lub $_SERVER['REQUEST_METHOD'] === 'POST';
  • przenieść dane z formularza do zapytania SQL (to już Lekcje 17–19, ale formularz z tej lekcji jest tam punktem wyjścia).

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

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

  1. name w polu formularza = klucz w $_GET / $_POST. Bez name pole nie istnieje dla PHP. id służy tylko etykiecie i CSS.
  2. Z formularza przychodzą wyłącznie teksty. Liczbę trzeba sprawdzić samodzielnie, zanim się jej użyje.
  3. if ($_SERVER['REQUEST_METHOD'] === 'POST') — tylko w środku tego if sięgamy po dane formularza.
  4. $_POST['klucz'] ?? '' zamiast gołego $_POST['klucz']. Puste pole to nie brak klucza.
  5. Niezaznaczonego checkboxa i radia nie ma w tablicy. Wiele wartości = name="cos[]".
  6. required / min / max da się obejść — PHP sprawdza wszystko jeszcze raz.
  7. Błąd użytkownika → tablica $bledy i formularz z zachowanymi danymi. Awaria → wyjątek.
  8. GET do czytania (filtry, wyszukiwanie, linki), POST do zmieniania danych (dodawanie, logowanie).
  9. Po udanym zapisie: header('Location: ...'); exit; — inaczej F5 zapisze drugi raz.
  10. header() musi być przed jakimkolwiek echo i przed HTML-em.

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

Formularz

<form method="post" action="dodaj-zadanie.php">   <!-- brak action = ten sam plik -->
    <label for="tytul">Tytuł</label>
    <input type="text" id="tytul" name="tytul" value="<?= $tytulZadania ?>" maxlength="100" required>

    <textarea name="opis" rows="3"><?= $opisZadania ?></textarea>   <!-- textarea NIE ma value -->

    <input type="date"   name="termin" value="<?= $terminZadania ?>">
    <input type="number" name="xp" min="1" max="100" value="<?= $xpZadania ?>">

    <input type="checkbox" name="zgoda" value="tak">            <!-- pojedynczy -->
    <input type="checkbox" name="food[]" value="pizza">         <!-- wiele -->
    <input type="radio"    name="plec" value="k">
    <select name="klasa"><option value="1">1</option></select>
    <select name="hobby[]" multiple size="4"> ... </select>
    <input type="hidden" name="filtr" value="aktywne">

    <button type="submit" name="przycisk">Wyślij</button>
</form>

Odbiór w PHP

$_GET['klucz']              // dane z adresu: strona.php?klucz=wartość
$_POST['klucz']             // dane z formularza method="post"
$_SERVER['REQUEST_METHOD']  // 'GET' albo 'POST'
$_FILES['plik']             // przesłany plik (formularz z enctype="multipart/form-data")

$tytul = $_POST['tytul'] ?? '';          // brak klucza → wartość domyślna
isset($_POST['przycisk'])                // czy klucz istnieje
is_array($_POST['food'] ?? null)         // czy przyszła tablica wartości
trim($tytul) === ''                      // puste pole (same spacje też)

Typowe wzorce

// 1. obsługa formularza tylko po wysłaniu
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tytul = trim($_POST['tytul'] ?? '');
    $xp    = trim($_POST['xp'] ?? '');

    $bledy = [];
    if ($tytul === '')                 { $bledy[] = "Tytuł jest wymagany."; }
    if (!preg_match('/^\d+$/', $xp))   { $bledy[] = "XP musi być liczbą całkowitą."; }
    elseif ((int) $xp < 1 || (int) $xp > 100) { $bledy[] = "XP musi być z zakresu 1–100."; }

    if (count($bledy) === 0) {
        // zapis + przekierowanie
    }
}

// 2. POST → przekierowanie → GET (F5 nie powtórzy zapisu)
header('Location: index.php?dodano=1');
exit;

// 3. parametr z adresu z wartością domyślną i listą dozwolonych wartości
$filtr = $_GET['filtr'] ?? 'wszystkie';
$lista = match ($filtr) {
    'aktywne'  => $aktywneZadania,
    'wykonane' => $wykonaneZadania,
    default    => $wszystkieZadania,
};

// 4. data poprawna kalendarzowo
checkdate(10, 2, 2026);   // miesiąc, dzień, rok → true/false

Pułapki

ZapisCo się stanie
<input type="text" id="xp">brak name → $_POST['xp'] nie istnieje
$_POST['xp'] ?? 10 przy pustym poluwynik to '', nie 10
(int) $_POST['xp'] bez sprawdzenia"12abc" → 12, "dużo" → 0
<textarea value="...">textarea nie ma value — tekst wpisz między znacznikami
echo "OK"; header('Location: ...');headers already sent, przekierowanie nie zadziała
header('Location: ...') bez exitreszta skryptu nadal się wykona
<a href="usun.php?id=3">zmiana danych przez GET — zrobi to odświeżenie lub robot

Quiz – sprawdź się

1. Użytkownik wszedł pod adres index.php?filtr=aktywne&strona=2. Co dokładnie znajdzie się w $_GET i jakiego typu są tam wartości?

2. Pole wygląda tak: <input type="text" id="tytul">. Formularz wysłano metodą POST. Co zwróci $_POST['tytul'] ?? '(brak)' i dlaczego?

3. Formularz wysłano metodą POST z pustym polem xp. Co wypisze kod?

$xp = $_POST['xp'] ?? 10;
var_dump($xp);

4. W formularzu jest <input type="checkbox" name="zgoda" value="tak">. Napisz warunek, który sprawdzi, czy użytkownik zaznaczył to pole. Dlaczego if ($_POST['zgoda'] === 'tak') to zły pomysł?

5. Uczeń chce odebrać wszystkie zaznaczone gatunki, ale w $_POST['gatunek'] jest zawsze tylko jedna wartość. Co trzeba zmienić?

<input type="checkbox" name="gatunek" value="rock">
<input type="checkbox" name="gatunek" value="pop">
<input type="checkbox" name="gatunek" value="jazz">

6. Znajdź błąd:

<textarea name="opis" rows="3" value="<?= $opisZadania ?>"></textarea>

7. Co zapisze ten kod dla wpisanego XP 12abc, a co dla dużo? Jak to naprawić?

$xp = (int) ($_POST['xp'] ?? 0);
if ($xp >= 1 && $xp <= 100) {
    $bledy = [];
} else {
    $bledy[] = "XP musi być z zakresu 1–100.";
}

8. Uzupełnij kod tak, aby wykonał się tylko po wysłaniu formularza:

if ($_SERVER['____________'] === '______') {
    // obsługa formularza
}

9. Dla każdej operacji wybierz metodę (GET / POST) i uzasadnij jednym zdaniem: a) wyszukiwanie zadań po słowie, b) oznaczenie zadania jako wykonanego, c) logowanie, d) przełączenie widoku listy na „aktywne”.

10. Dlaczego ten kod nie przekieruje użytkownika?

require_once 'includes/header.php';
zapiszZadania(PLIK_ZADAN, $zadania);
header('Location: index.php?dodano=1');
exit;

11. Po dodaniu zadania użytkownik naciska F5 i zadanie pojawia się w pliku drugi raz. Który mechanizm tego nie zastosowano i jak dokładnie wygląda jego kod?

12. Formularz ma pole <input type="number" name="xp" min="1" max="100" required>. Podaj dwa sposoby, w jakie użytkownik może mimo to wysłać xp=-50. Co go zatrzyma?

13. Zadanie praktyczne. Napisz w jednym pliku szukaj.php: formularz wysyłany metodą GET z polem szukaj i przyciskiem, a nad nim kod PHP, który — jeśli w adresie jest parametr szukaj i nie jest pusty — wypisze Szukasz: <fraza>, a w przeciwnym razie Wpisz szukane słowo.. Po wyszukaniu wpisane słowo ma zostać w polu.

14. W treści zadania egzaminacyjnego jest: „Formularz wysyłający dane metodą bezpieczną do tego samego pliku”. Jak wygląda znacznik <form> spełniający ten wymóg?


Odpowiedzi
  1. ['filtr' => 'aktywne', 'strona' => '2'] — tablica asocjacyjna, w której obie wartości są tekstami (również '2').
  2. (brak). Pole bez atrybutu name w ogóle nie zostaje wysłane, więc klucza tytul w $_POST nie ma; ?? podstawia wartość domyślną.
  3. string(0) "". Klucz xp istnieje (pole zostało wysłane, tylko puste), więc ?? się nie uruchamia. Pustkę trzeba sprawdzić osobno, np. trim($_POST['xp'] ?? '') === ''.
  4. if (isset($_POST['zgoda'])) (lub if (($_POST['zgoda'] ?? '') === 'tak')). Niezaznaczony checkbox nie wysyła nic, więc odwołanie do $_POST['zgoda'] da Warning: Undefined array key.
  5. Nazwa musi być tablicowa: name="gatunek[]" w każdym polu. Wtedy w $_POST['gatunek'] będzie tablica zaznaczonych wartości (a przy braku zaznaczeń — klucza nie będzie wcale).
  6. <textarea> nie ma atrybutu value. Wartość wpisuje się między znacznikami: <textarea name="opis" rows="3"><?= $opisZadania ?></textarea>.
  7. Dla 12abc (int) da 12 — kod uzna dane za poprawne i zapisze 12 XP, choć użytkownik wpisał śmieci. Dla dużo da 0 — użytkownik zobaczy komunikat o zakresie 1–100, mimo że nie wpisał żadnej liczby. Naprawa: najpierw sprawdzić format regexem preg_match('/^\d+$/', $xp), dopiero potem zakres.
  8. $_SERVER['REQUEST_METHOD'] === 'POST'.
  9. a) GET — tylko czytanie, adres z wynikami da się zapisać w zakładkach i wysłać; b) POST — zmienia dane w pliku; c) POST — hasło w adresie trafiłoby do historii przeglądarki i logów serwera; d) GET — to wybór, co pokazać, niczego nie zmienia.
  10. require_once 'includes/header.php' wypisało już HTML, a nagłówki muszą pójść przed treścią. PHP zgłosi Warning: Cannot modify header information – headers already sent. Przekierowanie musi znaleźć się przed dołączeniem nagłówka strony.
  11. Wzorca POST → przekierowanie → GET. Po udanym zapisie: header('Location: index.php?dodano=1'); exit; Ostatnim żądaniem staje się wtedy zwykłe GET, więc F5 tylko wyświetla stronę.
  12. Np. dopisać do <form> atrybut novalidate, usunąć min i required w F12 → Elementy, albo wysłać żądanie bez przeglądarki (np. curl). Zatrzyma go dopiero sprawdzenie w PHP (walidujNoweZadanie()).
  13. Przykładowe rozwiązanie: <?php $szukane = trim($_GET['szukaj'] ?? ''); if ($szukane !== '') { echo "<p>Szukasz: $szukane</p>"; } else { echo "<p>Wpisz szukane słowo.</p>"; } ?> <form method="get" action="szukaj.php"> <input type="text" name="szukaj" value="<?= $szukane ?>"> <button type="submit">Szukaj</button> </form> Formularz wysyłany metodą GET, więc fraza zostaje w adresie (szukaj.php?szukaj=PHP) i wraca do pola przez value.
  14. <form method="post"> — „metoda bezpieczna” to POST, a brak atrybutu action oznacza wysłanie do tego samego pliku (można też napisać action="" albo wprost nazwę pliku, np. action="index.php").