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
ACD.throwprzerywa funkcję, reszta blokutry(czyliecho "B") jest pomijana, PHP skacze docatch, a po nim program działa dalej.- Brak słowa
throw. Samonew Exception(...)tworzy obiekt i go porzuca — program leci dalej i dodaje ujemną nagrodę. Poprawnie:throw new Exception("...");. - 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. - Wyjątek przerwałby pętlę i przeskoczył do
catch, więcfclose()za pętlą nigdy by się nie wykonało — plik zostałby otwarty.finallywykonuje się zawsze, także po wyjątku. - Nie.
TypeErrornależy do rodzinyError, a nieException—catch (Exception ...)go nie złapie. To zwykle błąd w kodzie, który poprawiamy, a nie łapiemy. getMessage(). Nie pokazujemygetFile()anigetTraceAsString()— ujawniają ścieżki na serwerze i strukturę kodu.
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:
- zapisanie wprowadzonych danych do pliku tekstowego,
-
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.:
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 windex.php.
Rozgrzewka — 4 pytania z Lekcji 12
Odpowiedz z pamięci, zanim zaczniesz:
- W bloku
trysą trzy linie kodu, a wyjątek poleciał w drugiej. Co stanie się z trzecią linią i dokąd „przeskoczy” PHP? - Dlaczego pusty plik z zadaniami nie rzuca wyjątku, a brak pliku — rzuca?
- Po co w
index.phpjest$zadania = [];przed blokiemtry? - 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
| Plik | Co się zmienia |
|---|---|
includes/funkcje_zadan.php | nowe funkcje walidujNoweZadanie() i nastepneIdZadania() |
dodaj-zadanie.php | nowa strona: formularz, sprawdzenie danych, zapis, przekierowanie |
index.php | link „+ Dodaj zadanie”, komunikat o dodaniu, wybór widoku listy przez $_GET |
includes/header.php | klasa .sukces i kilka reguł dla formularza |
includes/footer.php, diagnostyka.php | numer 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)
- Zrób kopię zapasową całego katalogu
taskquest/z Lekcji 12 — np. jakotaskquest_lekcja12/(obok, nie w środku). Przyda się do testu regresji. - Zrób kopię pliku
data/zadania.csvpoza katalogiemtaskquest/(np. na pulpicie, jakozadania_kopia.csv). Dziś będziemy dopisywać do niego testowe zadania, a na koniec wrócimy do oryginału. - Sprawdź, czy
index.phppokazuje 5 zadań, adiagnostyka.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:
- otworzyć
data/zadania.csvw edytorze, - sprawdzić, jakie jest największe
id, i wymyślić kolejne, - wpisać wiersz, nie gubiąc żadnego średnika i pamiętając o formacie
RRRR-MM-DD, - 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:
- sposobu, żeby przeglądarka wysłała dane do skryptu PHP,
- sposobu, żeby PHP te dane odebrał,
- sposobu, żeby PHP odróżnił „ktoś dopiero otworzył formularz” od „ktoś go właśnie wysłał”,
- sprawdzenia danych — przeglądarka potrafi wysłać wszystko,
- 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:
- Adres zmienił się na
brudnopis.php?imie=Kama&xp=50. Dane są widoczne w pasku adresu. var_dump($_GET)pokazuje tablicę asocjacyjną:["imie"] => "Kama",["xp"] => "50".xptostring(2) "50", a nie liczba — mimo że pole matype="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
namepola staje się kluczem w tablicy$_GET. Pole beznamew ogóle nie zostanie wysłane (atrybutidsłuży tylko do połączenia z<label for="...">), $_GETto 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śba | Metoda | Dlaczego |
|---|---|---|
| pokaż listę zadań / tylko aktywne | GET | niczego 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” | GET | j.w. — to tylko czytanie |
| dodaj zadanie | POST | zmienia dane; odświeżenie, link w historii albo robot indeksujący stronę nie mogą przypadkiem dodać zadania |
| oznacz zadanie jako wykonane | POST | zmienia dane |
| logowanie | POST | hasł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:
| Pole | Co wysyła | Przydatne atrybuty |
|---|---|---|
<input type="text"> | tekst | required, 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>atrybutnovalidate— 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:
| Sytuacja | Jak ją zgłaszamy | Dlaczego |
|---|---|---|
| pusty tytuł, XP = 500, termin 31 lutego | lista 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
namew 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żenieUndefined array key. - Traktowanie danych z formularza jak liczb —
$_POST['xp']to zawsze tekst.(int) "12abc"daje12, a(int) "dużo"daje0— rzutowanie „po cichu” zamienia śmieci w liczbę. - Poleganie tylko na
requiredimin/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 atrybutvalue—textareanie ma atrybutuvalue, 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:
- wyświetla formularz (metoda POST, wysyłany do
brudnopis.php) z dwoma polami:tytulixp— oba jakotype="text"i bezrequired, żeby przeglądarka niczego nie blokowała i całą pracę musiał wykonać PHP, - 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.,
- tytuł (po
- gdy są błędy — wypisuje je jako listę
<ul>, gdy ich nie ma — wypisujePrzyjęto zadanie: <tytuł> (<xp> XP), - 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 dane | Oczekiwany wynik |
|---|---|
| pierwsze wejście (GET) | sam pusty formularz, brak komunikatów, brak ostrzeżeń PHP |
| tytuł pusty, XP puste | dwa błędy: tytuł i „liczbą całkowitą” |
Nauka, 150 | jeden błąd: zakres 1–100; w polu tytułu nadal Nauka |
, abc | dwa błędy (same spacje to pusty tytuł) |
Nauka PHP, 30 | Przyję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).
| Pole | Reguła | Komunikat (dokładnie taki) |
|---|---|---|
| tytuł | nie może być pusty | Tytuł jest wymagany. |
| tytuł | najwyżej 100 znaków (sprawdzaj tylko, gdy nie jest pusty) | Tytuł może mieć najwyżej 100 znaków. |
| opis | nieobowiązkowy, ale najwyżej 300 znaków | Opis może mieć najwyżej 300 znaków. |
| termin | kształt RRRR-MM-DD | Termin musi być datą w formacie RRRR-MM-DD. |
| termin | dzień musi istnieć w kalendarzu (sprawdzaj tylko, gdy kształt jest poprawny) | Taki dzień nie istnieje w kalendarzu. |
| XP | same cyfry | XP musi być liczbą całkowitą. |
| XP | zakres 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):
require_once:config/config.php,includes/funkcje_tekstowe.php(będzie potrzebnawyczyscTytul()),includes/funkcje_zadan.php,includes/funkcje_plikow.php,- wartości początkowe:
$tytulZadania,$opisZadania,$terminZadania,$xpZadania(puste teksty),$bledy(pusta tablica),$bladZapisu(null— tu trafi komunikat wyjątku, jak$bladZadanwindex.php), $nazwaStrony = 'Dodaj zadanie — TaskQuest';i dołączenieheader.php,- nagłówek
<h2>Nowe zadanie</h2>, link powrotny doindex.php, - formularz: metoda POST,
action="dodaj-zadanie.php", polatytul(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, - 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:
- 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+/', ' ', ...)itrim():textareapozwala 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(),
- tytuł — istniejącą funkcją
- 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łówekLocationmówi: „tu nic nie ma, idź pod ten adres”.exitnatychmiast 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 senti 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:
dodaj-zadanie.php: w miejscu$zadanieDodane = true;(to usuń) wstaw przekierowanie naindex.php?dodano=1iexit. 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ą.index.php:- w bloku wywołań funkcji, po obliczeniu streaka, dodaj nowy blok
// --- parametry z adresu strony (GET) ---i w nim zmienną$zadanieDodane—truetylko wtedy, gdy w adresie jestdodano=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.
- w bloku wywołań funkcji, po obliczeniu streaka, dodaj nowy blok
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).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:
- W bloku
// --- parametry z adresu strony (GET) ---odczytaj parametrfiltrdo zmiennej$filtr. Brak parametru =wszystkie. - Każda inna wartość niż
aktywneczywykonane(np.?filtr=abc,?filtr=AKTYWNE,?filtr=) ma dawać widokwszystkie. Parametr z adresu to dane od użytkownika — nie ufamy mu. - Do wyboru listy użyj
match(znasz go zpobierzTytul()) i zapisz wynik w nowej zmiennej$zadaniaDoWyswietlenia. Brakuje posortowanej listy wykonanych — dodaj$wykonaneWgTerminuobok$aktywneWgTerminu. - Pętla
foreachwyświetlająca listę ma przechodzić po$zadaniaDoWyswietleniazamiast po$zadaniaWgTerminu. - Gdy w pliku są zadania, ale wybrany widok jest pusty (np. żadne nie jest wykonane) — wypisz
Brak zadań w tym widoku. - 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>";
}
| Nr | Dane (co jest nietypowe) | Oczekiwany wynik |
|---|---|---|
| 1 | wszystko poprawne | OK |
| 2 | pusty tytuł | Tytuł jest wymagany. |
| 3 | tytuł: 101 znaków | Tytuł może mieć najwyżej 100 znaków. |
| 4 | tytuł: 100 × ą | OK (100 znaków, mimo 200 bajtów — dlatego mb_strlen()) |
| 5 | opis: 301 znaków | Opis może mieć najwyżej 300 znaków. |
| 6 | pusty termin | Termin musi być datą w formacie RRRR-MM-DD. |
| 7 | termin 02.10.2026 | Termin musi być datą w formacie RRRR-MM-DD. |
| 8 | termin 2026-02-29 | Taki dzień nie istnieje w kalendarzu. (2026 nie jest przestępny) |
| 9 | termin 2028-02-29 | OK (2028 jest przestępny) |
| 10 | termin 2026-13-01 | Taki dzień nie istnieje w kalendarzu. |
| 11 | XP 0 | XP musi być z zakresu 1–100. |
| 12 | XP 1 | OK |
| 13 | XP 100 | OK |
| 14 | XP 101 | XP musi być z zakresu 1–100. |
| 15 | XP dużo | XP musi być liczbą całkowitą. |
| 16 | XP 12abc | XP musi być liczbą całkowitą. |
| 17 | XP -5 | XP musi być liczbą całkowitą. (minus to nie cyfra) |
| 18 | XP 7.5 | XP musi być liczbą całkowitą. |
| 19 | wszystko puste | Tytuł 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 id | Oczekiwany wynik |
|---|---|
| (pusta lista) | 1 |
1, 2, 3 | 4 |
3, 10, 7 | 11 |
1, 2, 4, 5 | 6 (a nie 5!) |
prawdziwe zadania: nastepneIdZadania(wczytajZadania(PLIK_ZADAN)) na oryginalnym pliku | 6 |
(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 wpisujesz | Oczekiwany efekt |
|---|---|
| nic (pusty formularz) | trzy komunikaty w czerwonej ramce, plik bez zmian |
tytuł Nauka formularzy , termin pusty, XP 30 | jeden komunikat (termin); w polu tytułu jest już Nauka formularzy (oczyszczony!), XP nadal 30 |
tytuł Nauka formularzy, poprawny termin, XP 30 | przejś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 5 | zadanie 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=1 | brak pytania o ponowne wysłanie, w pliku nadal 7 zadań |
| to samo zadanie jeszcze raz przez formularz | dodaje 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.
| Zmiana | Oczekiwany efekt po wysłaniu poprawnego formularza |
|---|---|
plik nazywa się zadania_x.csv | czerwona 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łówek | zadanie 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):
| Adres | Oczekiwany efekt |
|---|---|
index.php | Widok: wszystkie, 5 zadań |
index.php?filtr=aktywne | Widok: aktywne, 3 zadania, wszystkie z [ ], posortowane wg terminu |
index.php?filtr=wykonane | Widok: wykonane, 2 zadania z [X] |
index.php?filtr=abc | Widok: wszystkie, 5 zadań, brak błędów |
index.php?filtr=AKTYWNE | Widok: wszystkie (wielkość liter ma znaczenie) |
index.php?filtr=aktywne&dodano=1 | widok aktywnych i zielona ramka |
index.php?filtr=wykonane, gdy w pliku wszystkie zadania mają wykonane = 0 | Brak 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:
- usuń
brudnopis.php, - usuń atrybut
novalidatez formularza wdodaj-zadanie.php, jeśli został, - przywróć
data/zadania.csvz kopii (5 oryginalnych zadań) — albo zostaw zadania, które naprawdę chcesz mieć w TaskQuest, - odśwież
index.phpidiagnostyka.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$zadaniaDoWyswietleniazamiast po$zadaniaWgTerminu; nad listą linki widoków, liniaWidok: ...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 (wtry/catch, razem zwczytajZadania()).
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.phpjest 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 unikalneidi jest dopisywane dodata/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
Wpisujesz w formularzu (metoda GET)
Kamaw pole zname="imie"iid="pole1". Jak wygląda adres po wysłaniu i jaki klucz pojawi się w$_GET? Co się zmieni, gdy usuniesz z pola atrybutname?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'];Pole
echo $_POST['tytul'] ?? 'brak';
echo ($_GET['dodano'] ?? '') === '1' ? 'tak' : 'nie';
var_dump($_POST);xpmatype="number"imin="1". Podaj dwa sposoby, w jakie użytkownik może mimo to wysłaćxp=-50. Która część TaskQuest go zatrzyma?Dlaczego sprawdzamy XP regexem
/^\d+$/przed porównaniem z zakresem? Co zapisałby TaskQuest dla12abc, a jaki komunikat zobaczyłby użytkownik dladużo, gdyby regexa nie było?Dlaczego
walidujNoweZadanie()zwraca tablicę komunikatów, a nie rzuca wyjątku przy pierwszym błędzie — skoro wsprawdzWierszZadania()wyjątek był dobrym pomysłem?W
dodaj-zadanie.phpwywołaniawczytajZadania()izapiszZadania()są w jednym blokutry. 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.Dlaczego
nastepneIdZadania()szuka największegoid, zamiast liczyćcount($zadania) + 1? Podaj przykład listy, dla której te dwa sposoby dają różny wynik.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?
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=...?W
index.phpwartość$filtrpochodzi 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)?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:
- przyjmuje tylko żądania POST (dla GET od razu przekierowuje na
index.php), - sprawdza, czy
idskłada się z samych cyfr, - w
try: wczytuje zadania, znajduje zadanie o tymid(pętlaforeachz kluczem:foreach ($zadania as $klucz => $zadanie)), ustawia mu'wykonane' => trueprzez$zadania[$klucz]['wykonane'] = true;, zapisuje plik i przekierowuje naindex.php?wykonano=1, - gdy zadania o takim
idnie 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 parachklucz=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/$_POSTjest atrybutname, nieid. Pole beznamenie zostanie wysłane w ogóle. - Wszystko, co przychodzi, jest tekstem — także z
type="number"itype="date"(data zawsze w formacieRRRR-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 sprawdzamyisset()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
$_POSTtylko wtedy, gdy maname(<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 wif ($_SERVER['REQUEST_METHOD'] === 'POST') { ... }.??chroni przedWarning: Undefined array key:$tytul = $_POST['tytul'] ?? '';.- Uwaga na pułapkę:
??reaguje tylko na brak klucza albonull. Pusty tekst z formularza istnieje, więc$_POST['xp'] ?? 10dla pustego pola da'', a nie10.
Sprawdzanie danych
required,min,max,maxlengthto 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 itry/catch. - Rzutowanie
(int)nie jest walidacją:(int) "12abc"daje12,(int) "dużo"daje0. 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ę, aexitjest 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
actionalbo wpisać w nim nazwę bieżącego pliku; - zbudować pola z etykietami połączonymi z polem (
<label for="...">+idw 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:
namew polu formularza = klucz w$_GET/$_POST. Beznamepole nie istnieje dla PHP.idsłuży tylko etykiecie i CSS.- Z formularza przychodzą wyłącznie teksty. Liczbę trzeba sprawdzić samodzielnie, zanim się jej użyje.
if ($_SERVER['REQUEST_METHOD'] === 'POST')— tylko w środku tegoifsięgamy po dane formularza.$_POST['klucz'] ?? ''zamiast gołego$_POST['klucz']. Puste pole to nie brak klucza.- Niezaznaczonego checkboxa i radia nie ma w tablicy. Wiele wartości =
name="cos[]". required/min/maxda się obejść — PHP sprawdza wszystko jeszcze raz.- Błąd użytkownika → tablica
$bledyi formularz z zachowanymi danymi. Awaria → wyjątek. - GET do czytania (filtry, wyszukiwanie, linki), POST do zmieniania danych (dodawanie, logowanie).
- Po udanym zapisie:
header('Location: ...'); exit;— inaczej F5 zapisze drugi raz. header()musi być przed jakimkolwiekechoi 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
| Zapis | Co się stanie |
|---|---|
<input type="text" id="xp"> | brak name → $_POST['xp'] nie istnieje |
$_POST['xp'] ?? 10 przy pustym polu | wynik 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 exit | reszta 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
['filtr' => 'aktywne', 'strona' => '2']— tablica asocjacyjna, w której obie wartości są tekstami (również'2').(brak). Pole bez atrybutunamew ogóle nie zostaje wysłane, więc kluczatytulw$_POSTnie ma;??podstawia wartość domyślną.string(0) "". Kluczxpistnieje (pole zostało wysłane, tylko puste), więc??się nie uruchamia. Pustkę trzeba sprawdzić osobno, np.trim($_POST['xp'] ?? '') === ''.if (isset($_POST['zgoda']))(lubif (($_POST['zgoda'] ?? '') === 'tak')). Niezaznaczony checkbox nie wysyła nic, więc odwołanie do$_POST['zgoda']daWarning: Undefined array key.- 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). <textarea>nie ma atrybutuvalue. Wartość wpisuje się między znacznikami:<textarea name="opis" rows="3"><?= $opisZadania ?></textarea>.- Dla
12abc(int)da12— kod uzna dane za poprawne i zapisze 12 XP, choć użytkownik wpisał śmieci. Dladużoda0— użytkownik zobaczy komunikat o zakresie 1–100, mimo że nie wpisał żadnej liczby. Naprawa: najpierw sprawdzić format regexempreg_match('/^\d+$/', $xp), dopiero potem zakres. $_SERVER['REQUEST_METHOD'] === 'POST'.- 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.
require_once 'includes/header.php'wypisało już HTML, a nagłówki muszą pójść przed treścią. PHP zgłosiWarning: Cannot modify header information – headers already sent. Przekierowanie musi znaleźć się przed dołączeniem nagłówka strony.- 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ę. - Np. dopisać do
<form>atrybutnovalidate, usunąćminirequiredw F12 → Elementy, albo wysłać żądanie bez przeglądarki (np.curl). Zatrzyma go dopiero sprawdzenie w PHP (walidujNoweZadanie()). - 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 przezvalue. <form method="post">— „metoda bezpieczna” to POST, a brak atrybutuactionoznacza wysłanie do tego samego pliku (można też napisaćaction=""albo wprost nazwę pliku, np.action="index.php").