INF.03-05-26.01-SGStyczeń 2026 · 52 kroki · ok. 97 min · 39 kryteriów z klucza
Rozwiązanie krok po kroku: Portal zgłoszeń wypadków (Nowoczesny kod (grid, PDO))
To drugi sposób rozwiązania tego samego arkusza: portal zgłoszeń wypadków, baza zgloszenia, jedna strona index.php z tabelą personelu wybieranego polem opcji, listą osób bez zgłoszeń i formularzem nowego zgłoszenia. Obie wersje spełniają wszystkie 39 kryteriów klucza - różni je sposób pisania kodu, nie wynik.
Czym ta wersja różni się od podstawowej:
siatka CSS w bloku głównym (grid-template-columns: 50% 50%), kolory w zmiennych CSS, tabela z <thead> i <tbody>,
PHP na PDO z wyjątkami, cała logika na górze pliku, a w HTML tylko pętle foreach i funkcja escape(),
opcja z formularza z białej listy i zapytanie przygotowane - wartość spoza „policjant/ratownik” nie dotrze do SQL,
zapytanie 3 przez NOT EXISTS zamiast NOT IN - z wyjaśnieniem, kiedy NOT IN zawodzi,
zapis zgłoszenia z filter_var, obsługą błędu klucza obcego (osoba spoza bazy) i wzorcem PRG: po zapisie przekierowanie, więc lista od razu jest aktualna, a F5 nie dopisze zgłoszenia drugi raz.
Kroki nadal idą zdanie po zdaniu za treścią arkusza. Podgląd w każdym kroku z PHP to wynik prawdziwego uruchomienia kodu z tego kroku w PHP 8.2 na bazie z arkusza; od kroku z polem opcji pokazuje stan z ilustracji 2 - po wybraniu „Ratownik” i kliknięciu „Pokaż”. Baza pochodzi z prawdziwego archiwum pliki7.
Jak liczą się punkty. Klucz ma 39 kryteriów (R.1.1 - R.5.9), każde za 1 punkt. Próg zdania części praktycznej to 75%, czyli 30 punktów. Przy każdym kroku jest napisane, które kryteria spełnia; egzaminator ocenia spełnienie kryteriów z klucza, nie zgodność z tym kodem.
Suwak i strzałki obok niego - albo ←→ na klawiaturze - przechodzą krok po kroku; wyjaśnienie, pliki i podgląd zmieniają się razem z krokiem.
Lista „Etap” przenosi od razu do wybranego etapu.
„Rozwiązuję samodzielnie” chowa rozwiązanie kroku - najpierw własna próba, potem odsłonięcie.
„Tryb prezentacji” pokazuje same kroki, bez menu strony - na rzutnik.
Etap 1: Baza danych
Kroki 1-7 · po tym etapie masz 6 z 39 kryteriów (pkt)
Krok 1: Baza danych zgloszenia
ok. 2 min
Polecenie z arkusza
Utwórz bazę danych o nazwie zgloszenia, z zestawem polskich znaków (np. utf8_polish_ci)
Archiwum pliki7.7z ma hasło @LarMowe^ (małpa na początku, daszek na końcu). W phpMyAdmin: Nowa, nazwa zgloszenia - bez „ł”, dokładnie jak w arkuszu - porównywanie utf8_polish_ci. To samo daje polecenie w zakładce SQL:
CREATE DATABASE zgloszenia CHARACTER SET utf8 COLLATE utf8_polish_ci;
Typowe błędy
Nazwa z polską literą (zgłoszenia) to inna baza niż ta, z którą łączą się skrypty - przepada punkt za połączenie (R.5.1).
Krok 2: Import tabel z pliku zgloszenia.sql
ok. 2 min
Polecenie z arkusza
Z rozpakowanego archiwum zaimportuj tabele z pliku zgloszenia.sql do utworzonej bazy
Zaznacz bazę zgloszenia, potem Import → zgloszenia.sql → Wykonaj. Powstają trzy tabele z ilustracji 1: personel (18 osób, status „policjant” albo „ratownik”), pojazdy (30 pojazdów, typ „K” - karetka, „R” - radiowóz) i rejestr (29 zgłoszeń). rejestr kluczami obcymi wskazuje osobę i pojazd - baza nie przyjmie zgłoszenia dla osoby, której nie ma w personel, co przyda się w Skrypcie 3.
Krok 3: Zrzut ekranu import.png
Kryteria:
R.1.1 Wykonany import tabel do bazy danych zgloszenia - plik import.png
ok. 2 min
Polecenie z arkusza
Wykonaj zrzut ekranu po imporcie. Zapisz zrzut w formacie PNG pod nazwą import. Nie kadruj zrzutu. Powinien on obejmować cały ekran monitora, z widocznym paskiem zadań. Na zrzucie powinny być widoczne elementy wskazujące na poprawnie wykonany import tabel
Print Screen (cały ekran z paskiem zadań), wklej do edytora grafiki i zapisz jako import.png. Uwaga na formaty: import w PNG, zrzuty zapytań w JPEG.
Typowe błędy
Wykadrowany zrzut albo zrzut bez paska zadań nie jest oceniany (uwaga w kluczu przy R.1).
Krok 4: Zapytanie 1: policjanci
Kryteria: ,
R.1.2 W pliku kwerendy.txt zapisane co najmniej jedno zapytanie SQL, wynikające z treści zadania
R.1.3 Wykonane zapytanie 1 wybierające jedynie identyfikatory, imiona i nazwiska tych osób spośród personelu, dla których status to policjant. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT id, imie, nazwisko FROM personel WHERE status = "policjant"; oraz na zrzucie widocznych jest dokładnie 11 rekordów z id: 1, 2, 5, 6, 7, 8, 10, 11, 14, 16, 18
ok. 2 min
Polecenie z arkusza
Zapytanie 1: wybierające jedynie identyfikatory, imiona i nazwiska tych osób spośród personelu, których status to policjant
SELECT id, imie, nazwisko - trzy pola, bez statusu,
WHERE status = 'policjant' - tekst w apostrofach.
Wynik: 11 wierszy o identyfikatorach 1, 2, 5, 6, 7, 8, 10, 11, 14, 16, 18 (R.1.3). Zapytania zapisuj w kwerendy.txt z komentarzem -- nad każdym, zrzut jako kw1.jpg. W Skrypcie 1 w miejscu słowa „policjant” stanie parametr z opcją z formularza.
kwerendy.txtTekstnowy plik
-- Zapytanie 1SELECT id, imie, nazwiskoFROM personelWHERE status = 'policjant';
Podgląd strony po tym kroku
Krok 5: Zapytanie 2: liczba karetek i radiowozów
Kryteria:
R.1.4 Wykonane zapytanie 2 wybierające jedynie typ i zliczające liczbę pojazdów zgrupowanych według typu. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT typ, COUNT(*) FROM pojazdy GROUP BY typ; oraz na zrzucie widoczne są dane: K 6; R 24
ok. 2 min
Polecenie z arkusza
Zapytanie 2: wybierające jedynie typ i zliczające liczbę pojazdów zgrupowanych według typu (zapytanie wybiera liczbę karetek i radiowozów)
GROUP BY typ - wiersze o tym samym typie tworzą jedną grupę,
COUNT(*) AS liczba - liczba wierszy w grupie, z aliasem, żeby kolumna wyniku nie nazywała się COUNT(*).
Wynik: K 6 i R 24 (R.1.4). Zrzut: kw2.jpg.
Typowe błędy
SELECT COUNT(*) FROM pojazdy bez grupowania zwraca jedną liczbę 30 - nie widać, ile jest karetek, a ile radiowozów.
kwerendy.txtTekstzmienione: 5 linii
-- Zapytanie 1SELECT id, imie, nazwiskoFROM personelWHERE status = 'policjant';-- Zapytanie 2SELECT typ, COUNT(*) AS liczbaFROM pojazdyGROUP BY typ;
Podgląd strony po tym kroku
Krok 6: Zapytanie 3: osoby bez żadnego zgłoszenia
Kryteria:
R.1.5 Wykonane zapytanie 3 wybierające jedynie te identyfikatory i nazwiska osób spośród personelu, których id nie pojawia się w rejestrze. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT id, nazwisko FROM personel WHERE id NOT IN (SELECT id_personel FROM rejestr); lub SELECT personel.id, nazwisko FROM personel LEFT JOIN rejestr ON personel.id = id_personel GROUP BY personel.id HAVING COUNT(data) = 0; (lub inne rozwiązania dające uniwersalny wynik) oraz na zrzucie widoczne są dokładnie 3 rekordy: 2 Nowak, 3 Nosowska, 4 Mizera. (Baza musi być przywrócona do stanu początkowego)
ok. 4 min
Polecenie z arkusza
Zapytanie 3: wybierające jedynie te identyfikatory i nazwiska osób spośród personelu, których id nie pojawia się w rejestrze
FROM personel AS p - alias p do odwołań z podzapytania,
WHERE NOT EXISTS (SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id) - zostaje osoba, dla której nie istnieje ani jeden wiersz rejestru z jej identyfikatorem. SELECT 1 niczego nie wybiera - podzapytanie odpowiada tylko „jest / nie ma”.
Wynik: 2 Nowak, 3 Nosowska, 4 Mizera (R.1.5); klucz przyjmuje „inne rozwiązania dające uniwersalny wynik”. Klucz podaje też wersję z NOT IN (SELECT id_personel FROM rejestr) - daje tu ten sam wynik, ale ma pułapkę: gdyby w kolumnie podzapytania trafił się choć jeden NULL, NOT IN zwróciłoby zero wierszy, bo porównanie z NULL nie jest ani prawdą, ani fałszem. W tej bazie id_personel ma NOT NULL, więc problem nie wystąpi, a NOT EXISTS jest odporne zawsze. Zrzut: kw3.jpg.
Typowe błędy
Zapytanie 3 wykonaj przed zapytaniem 4 albo na bazie w stanie początkowym (uwaga w kluczu) - po dopisaniu zgłoszeń wynik się zmienia.
kwerendy.txtTekstzmienione: 7 linii
-- Zapytanie 1SELECT id, imie, nazwiskoFROM personelWHERE status = 'policjant';-- Zapytanie 2SELECT typ, COUNT(*) AS liczbaFROM pojazdyGROUP BY typ;-- Zapytanie 3SELECT p.id, p.nazwiskoFROM personel AS pWHERE NOT EXISTS ( SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id);
Podgląd strony po tym kroku
Krok 7: Zapytanie 4: nowe zgłoszenie z dzisiejszą datą
Kryteria:
R.1.6 Wykonane zapytanie 4 wstawiające rekord do tabeli rejestr : identyfikator personelu: 1, identyfikator pojazdu: 14, automatycznie wpisywana aktualna data, klucz generowany automatycznie. W pliku z kwerendami lub na zrzucie istnieje zapis np.: INSERT INTO rejestr(data, id_personel, id_pojazd) VALUES (CURDATE(), 1, 14); ( możliwe też CURRENT_DATE() i NOW()) oraz na zrzucie jest komunikat o poprawnie wykonanym zapytaniu lub widoczny wstawiony wiersz
ok. 2 min
Polecenie z arkusza
Zapytanie 4: wstawiające rekord do tabeli rejestr: identyfikator personelu: 1, identyfikator pojazdu: 14, automatycznie wpisywana aktualna data, klucz generowany automatycznie
Lista kolumn bez id - AUTO_INCREMENT nada numer 30. CURDATE() wstawia datę serwera w chwili wykonania zapytania (klucz przyjmuje też CURRENT_DATE() i NOW(); NOW() niesie godzinę, którą kolumna DATE i tak odetnie). Zrzut kw4.jpg: komunikat o wstawieniu albo tabela z nowym wierszem (R.1.6).
Typowe błędy
Data wpisana ręcznie ('2026-01-12') nie jest „automatycznie wpisywaną aktualną datą” - jutro to samo zapytanie wstawi wczorajszy dzień.
kwerendy.txtTekstzmienione: 4 linie
-- Zapytanie 1SELECT id, imie, nazwiskoFROM personelWHERE status = 'policjant';-- Zapytanie 2SELECT typ, COUNT(*) AS liczbaFROM pojazdyGROUP BY typ;-- Zapytanie 3SELECT p.id, p.nazwiskoFROM personel AS pWHERE NOT EXISTS ( SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id);-- Zapytanie 4INSERT INTO rejestr (data, id_personel, id_pojazd)VALUES (CURDATE(), 1, 14);
Podgląd strony po tym kroku
Etap 2: Strona HTML
Kroki 8-26 · po tym etapie masz 17 z 39 kryteriów (pkt)
Krok 8: Plik index.php w HTML5
ok. 2 min
Polecenie z arkusza
Składa się ze strony o nazwie index.php. Zapisana w języku HTML5
<!DOCTYPE html> i szkielet strony. Rozszerzenie .php, bo dojdą skrypty; plik leży w htdocs XAMPP-a i otwiera się go przez http://localhost/....
R.2.2 W sekcji head znajduje się jawnie podany standard kodowania polskich znaków i tytuł strony "ZGŁOSZENIA”
ok. 1 min
Polecenie z arkusza
Tytuł strony „ZGŁOSZENIA”
<title> wielkimi literami, jak w arkuszu (z kodowaniem - R.2.2). <meta name="viewport"> pokazuje stronę na telefonie w jego prawdziwej szerokości; arkusz tego nie wymaga.
R.2.3 W sekcji body zdefiniowany jest podział na bloki za pomocą semantycznych znaczników HTML5: header, main->section+section, footer (dopuszcza się zastosowanie bloków pomocniczych)
ok. 2 min
Polecenie z arkusza
Podział strony na bloki zrealizowany za pomocą semantycznych znaczników bloków języka HTML5 tak, aby po uruchomieniu w przeglądarce układ bloków na stronie był zgodny z ilustracją 3
Klucz R.2.3 wymienia znaczniki: header, main z dwiema section, footer. Sekcje są jedynymi dziećmi <main>, więc siatka na <main> ustawi je obok siebie.
Identyfikatory opisują zawartość sekcji, nie ich miejsce: staff (personel) i new-report (nowe zgłoszenie). left/right przestałyby być prawdą po pierwszej zmianie układu, np. na telefonie, gdzie sekcje stanęłyby jedna pod drugą.
R.2.5 Na stronie umieszczony przynajmniej jeden formularz, zastosowany znacznik <form>
R.3.3 Przynajmniej jeden formularz wysyła dane metodą POST do tego samego pliku (poprawne również bez atrybutu action)
ok. 2 min
Polecenie z arkusza
Formularz wysyłający dane do tego samego pliku metodą bezpieczną
<form method="post"> bez action - brak action to „ten sam adres” (R.2.5, R.3.3). „Metoda bezpieczna” w arkuszu to POST: dane w treści żądania, a nie w adresie. Ten formularz tylko wybiera, co pokazać, więc w prawdziwym serwisie pasowałby do niego GET (adres z wybraną opcją dałoby się zapisać w zakładkach) - ale arkusz i klucz chcą POST, a klucz ocenia właśnie to.
Krok 17: Pole opcji „Policjant”, domyślnie zaznaczone
ok. 2 min
Polecenie z arkusza
Pole opcji z treścią „Policjant”, domyślnie zaznaczone
<input type="radio"> z checked - zaznaczone przy pierwszym wejściu. Treść „Policjant” jest w <label for="police">, więc kliknięcie napisu też zaznacza pole, a czytnik ekranu czyta napis jako nazwę pola. value="policjant" - małymi literami, tak jak status jest zapisany w bazie; tę wartość dostanie skrypt.
W jednym momencie można zaznaczyć tylko jedno pole opcji
Kod z dwóch poprzednich kroków już to zapewnia: oba pola mają ten sam name="option", a przeglądarka traktuje pola opcji o tej samej nazwie jako jedną grupę - zaznaczenie „Ratownik” odznacza „Policjant”. Z tym krokiem jest komplet R.3.5 (z polem liczbowym, które dojdzie niżej). Dla czytnika ekranu grupę można by jeszcze opisać <fieldset> z <legend>; na ilustracji 2 nie ma ramki ani podpisu grupy, więc zostaje bez nich.
Typowe błędy
Różne name w polach opcji - obie kropki da się zaznaczyć naraz, a skrypt dostanie tylko jedną wartość.
R.2.9 Pierwszy wiersz tabeli zawiera komórki nagłówkowe „Id”, „Imię”, „Nazwisko”, zastosowane znaczniki <tr> i <th>
ok. 2 min
Polecenie z arkusza
Tabela z trzema kolumnami, w której pierwszy wiersz zawiera komórki nagłówkowe: „Id”, „Imię”, „Nazwisko”. Pozostałe wiersze wypełnione są przez skrypt 1
Pierwszy wiersz to <tr> z trzema komórkami nagłówkowymi <th> (R.2.9). Wiersz nagłówkowy stoi w <thead>, a miejsce na dane - w <tbody>: przeglądarka wie, który wiersz opisuje kolumny (przy drukowaniu długiej tabeli powtórzy go na każdej stronie), a skrypt będzie dopisywał wiersze do <tbody>, nie mieszając ich z nagłówkiem.
Krok 22: Sekcja nowego zgłoszenia: nagłówek i lista
ok. 1 min
Polecenie z arkusza
Zawartość bloku sekcji prawej: Nagłówek drugiego stopnia o treści „Nowe zgłoszenie”. Lista numerowana (uporządkowana) wypełniona przez skrypt 2
<h2> i pusta lista uporządkowana <ol> - elementy <li> wypisze Skrypt 2. Numerów 1., 2., 3. na ilustracji 2 nie ma: zdejmie je CSS, bo przed nazwiskiem i tak stoi identyfikator osoby.
Formularz wysyłający dane do tego samego pliku metodą bezpieczną
Osobny <form method="post"> - każdy formularz wysyła tylko swoje pola, więc po „Dodaj zgłoszenie” w $_POST będzie sam identyfikator osoby, bez opcji z lewej sekcji. Tak skrypt rozpozna, który formularz kliknięto. Ten formularz zmienia dane w bazie, więc tu POST jest jedynym właściwym wyborem.
R.3.5 Pierwsze pole opcji jest domyślnie zaznaczone (checked) , w jednym momencie można wybrać tylko jedno pole opcji, pole edycyjne jest typu number
ok. 2 min
Polecenie z arkusza
Pole etykiety „Wybierz id osoby z listy: ”, powiązane z polem edycyjnym. Pole edycyjne przeznaczone do wpisywania liczb
<label for="person"> wskazuje id pola. Pole type="number" (R.3.5) z min="1", step="1" i required - identyfikatory to dodatnie liczby całkowite, a pusty formularz nie wyjdzie z przeglądarki. To wygoda, nie zabezpieczenie: Skrypt 3 i tak sprawdzi wartość. name="person" to klucz w $_POST.
R.2.4 W bloku nagłówkowym znajduje się <h1>: „Zgłoszenia wydarzeń”, w sekcji lewej <h2>: „Personel", w sekcji prawej <h2>: „Nowe zgłoszenie” oraz <p> z numerem zdającego w stopce
ok. 1 min
Polecenie z arkusza
Zawartość stopki: paragraf o treści „Stronę wykonał: ”, dalej wstawiony numer zdającego
Akapit w <footer>. Tu stoi imię i nazwisko autora rozwiązania - na egzaminie wpisujesz swój numer zdającego (PESEL). Są już nagłówki i akapit z R.2.4: <h1>, dwa <h2> i <p> w stopce.
Kroki 27-38 · po tym etapie masz 28 z 39 kryteriów (pkt)
Krok 27: Zmienne CSS z kolorami arkusza
ok. 2 min
Polecenie z arkusza
Styl CSS zdefiniowany jest w całości w zewnętrznym pliku o nazwie styl.css.
Maroon pojawi się w sześciu miejscach: tło nagłówka, stopki i przycisków, ramki tabeli i komórek, napis przycisku pod kursorem. Wpisany raz w --color-maroon, w regułach jest używany jako var(--color-maroon). Tła sekcji też dostają zmienne z nazwami od zawartości sekcji.
R.4.1 domyślne dla wszystkich selektorów (*): krój czcionki Tahoma
ok. 1 min
Polecenie z arkusza
Domyślnie dla wszystkich selektorów: krój czcionki Tahoma
Selektor * (R.4.1) obejmuje też przyciski i pola, które domyślnie mają krój systemowy. Arkusz nie podaje rodziny zapasowej, a klucz sprawdza samą Tahomę, więc zostaje bez dopisku sans-serif.
Wspólne dla bloku nagłówkowego i stopki: kolor tła Maroon, biały kolor czcionki, marginesy wewnętrzne 10 px
Jedna reguła dla dwóch selektorów. Wyrównania do środka tu nie ma - arkusz każe wyśrodkować nagłówki h1-h3, a nie cały blok, dlatego podpis w stopce stoi przy lewej krawędzi, jak na ilustracji 2.
R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z ilustracją 3 w arkuszu egzaminacyjnym (prawidłowo zastosowano właściwości CSS decydujące o układzie bloków na stronie, np. float i clear albo display flex albo grid albo position)
ok. 2 min
Polecenie z arkusza
Podział strony na bloki zrealizowany za pomocą semantycznych znaczników bloków języka HTML5 tak, aby po uruchomieniu w przeglądarce układ bloków na stronie był zgodny z ilustracją 3
<main> staje się siatką o dwóch kolumnach po 50%: sekcja personelu w pierwszej, nowe zgłoszenie w drugiej. Nagłówek i stopka są poza siatką, więc stoją nad i pod nią - bez float i clear (R.3.1). Szerokości sekcji w grid-template-columns zamykają R.4.4 razem z wysokością 500 px i szerokością tabeli, która dojdzie za chwilę.
Typowe błędy
Odstęp gap przy kolumnach 50% + 50% wypchnąłby siatkę poza ekran - przy procentach z arkusza odstępów nie ma.
Krok 35: Tabela: szerokość, jedno obramowanie, wyśrodkowanie
Kryteria: ,
R.4.4 wysokość: blok sekcji lewej i prawej - 500px, szerokość: blok sekcji lewej i prawej - 50% (width lub grid-template-columns lub flex) , selektor table - 85%
R.4.8 obramowanie: tabeli i selektora td - border: 1px solid Maroon; oraz dla tabeli krawędzie połączone: border-collapse: collapse;
ok. 2 min
Polecenie z arkusza
Dodatkowo dla selektora tabeli: szerokość 85%, krawędzie połączone w jedno obramowanie, marginesy zewnętrzne automatycznie wyliczone przez przeglądarkę
border-collapse: collapse skleja ramki tabeli i komórek w jedną linię (R.4.8). margin: auto rozdziela wolne miejsce po równo z lewej i prawej - tabela o szerokości 85% staje na środku sekcji. Komplet wymiarów R.4.4.
R.4.2 kolor tła: blok nagłówkowy, stopka i oba przyciski - Maroon, blok sekcji lewej - LightSalmon, blok sekcji prawej - DarkSalmon
R.4.3 kolor czcionki: blok nagłówkowy, stopka, oba przyciski - biały
R.4.6 marginesy wewnętrzne (padding) : blok nagłówkowy, stopka, oba przyciski - 10px marginesy zewnętrzne (margin) : tabela - auto, jedynie margin-left dla obu przycisków - 20px
R.4.9 styl obu przycisków: bez obramowania np. border: none; oraz border-radius: 20px;
ok. 2 min
Polecenie z arkusza
Dla obu przycisków: kolor tła Maroon, biały kolor czcionki, jedynie margines zewnętrzny lewy 20 px, marginesy wewnętrzne 10 px, bez obramowania, zaokrąglenie rogów 20 px
Selektor button obejmuje oba przyciski. „Jedynie margines zewnętrzny lewy” to margin-left: 20px - margin: 20px odsunąłby przycisk ze wszystkich stron. Ta reguła zamyka kolory tła (R.4.2), kolor czcionki (R.4.3), marginesy (R.4.6) i styl przycisków (R.4.9).
R.4.10 styl, gdy kursor znajdzie się na przycisku jego kolor tła zamienia się na biały, a kolor czcionki na Maroon
ok. 1 min
Polecenie z arkusza
Gdy kursor znajdzie się na przycisku jego kolor tła zmienia się na biały, a kolor czcionki na Maroon
Pseudoklasa :hover zamienia kolory miejscami (R.4.10). Ta sama zamiana dla :focus-visible: przycisk bez ramki (border: none) po wybraniu klawiszem Tab musi być jakoś wyróżniony, inaczej osoba korzystająca z klawiatury nie wie, gdzie jest.
Kroki 39-49 · po tym etapie masz 39 z 39 kryteriów (pkt)
Krok 39: Połączenie PDO z obsługą błędu i zamknięcie
Kryteria:
R.5.1 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy zgloszenia oraz ostatnią operacją na bazie jest jej zamknięcie
ok. 4 min
Polecenie z arkusza
Łączą się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie zgloszenia. Na końcu zamykane jest połączenie z serwerem
Cały PHP stoi na górze pliku, przed <!DOCTYPE html> - za chwilę dojdzie przekierowanie, a nagłówki HTTP wysyła się przed treścią strony. new PDO(...) z charset=utf8mb4 i dwiema opcjami: ERRMODE_EXCEPTION (błąd SQL rzuca wyjątek, który da się złapać) i FETCH_ASSOC (wiersze z nazwami kolumn). try/catch zamienia nieudane połączenie na ogólny komunikat z kodem 500.
Na dole bloku zamknięcie: $statement = null; (obiekt zapytania trzyma połączenie) i $pdo = null;. Zapytania będą dopisywane nad tymi liniami, więc zamknięcie zostaje ostatnią operacją na bazie (R.5.1). Od tego kroku podgląd to wynik prawdziwego uruchomienia w PHP 8.2.
Typowe błędy
PDO nie ma w tabeli 1 arkusza - nazwy stałych trzeba znać na pamięć. Kto nie jest pewny składni, pisze funkcjami mysqli_* z tabeli 1 (wersja podstawowa).
index.phpPHPzmienione: 16 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"checked> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 40: Funkcja escape() do bezpiecznego wypisywania
ok. 2 min
Polecenie z arkusza
Należy stosować znaczące nazewnictwo zmiennych i funkcji w języku polskim lub angielskim
escape() przepuszcza tekst przez htmlspecialchars z ENT_QUOTES: <, >, & i oba cudzysłowy zamieniają się na encje. Każde nazwisko z bazy i każdy komunikat przechodzi przez nią przed wypisaniem - wpis <script> w tabeli personel pokaże się jako tekst, zamiast wykonać się w przeglądarce (XSS). Nazwa to pełne słowo, a nie frameworkowy skrót e(), bo arkusz wymaga nazw mówiących, do czego służą.
index.phpPHPzmienione: 5 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"checked> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Krok 41: Skrypt 1: opcja z białej listy i zapytanie 1
Kryteria: ,
R.5.3 Skrypt 1 zawiera instrukcję wysyłającą do bazy zmodyfikowane zapytanie 1 w ten sposób, że wybierane są rekordy z opcją wysłaną z formularza (policjant lub ratownik) lub w stanie początkowym z opcją policjant
R.5.8 Przynajmniej jeden skrypt zawiera instrukcję pobierającą dane z tablicy globalnej odpowiadającej zastosowanej metodzie formularza ($_POST, $_GET, $_REQUEST)
ok. 4 min
Polecenie z arkusza
Wysyła do bazy danych zapytanie 1, zmodyfikowane w ten sposób, że wybierane są rekordy z opcją wysłaną z formularza (policjant lub ratownik) lub w stanie początkowym z opcją policjant
Wybrane pole opcji jest w $_POST['option'] (R.5.8). Operator ?? podstawia „policjant”, gdy formularza nikt nie wysłał - to stan początkowy z arkusza.
Formularz pozwala wybrać tylko dwie wartości, ale żądanie POST da się wysłać z pominięciem formularza. Stąd biała lista: in_array($option, $allowedOptions, true) sprawdza, czy wartość jest jedną z dwóch dozwolonych (true - porównanie ścisłe, także typu), a inna wraca do „policjant”. Do tego zapytanie przygotowane z parametrem :status (R.5.3) - dwie warstwy: biała lista pilnuje sensu, parametr pilnuje składni SQL. W wersji podstawowej z egzaminu wartość ' OR '1'='1 pokazywała cały personel.
Podgląd od tego kroku: strona po wybraniu „Ratownik” i kliknięciu „Pokaż”, jak na ilustracji 2 - wiersze dojdą w kolejnym kroku.
index.phpPHPzmienione: 10 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"checked> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
R.5.4 Skrypt 1 wyświetla <h3> o treści „Wybrano opcję: <opcja>”, gdzie <> oznacza wybraną w formularzu opcję
ok. 1 min
Polecenie z arkusza
Wypisuje nagłówek trzeciego stopnia o treści „Wybrano opcję: <opcja>”, gdzie <opcja> oznacza wybraną w formularzu opcję. Element ten należy wypisać przed tabelą.
<h3> stoi w HTML między formularzem a tabelą, więc jest „przed tabelą” (R.5.4). Wypisuje $option po białej liście - „ratownik” małą literą, jak na ilustracji 2. Wyśrodkowanie dała już reguła h3.
index.phpPHPzmienione: 1 linia
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"checked> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 43: Skrypt 1: wiersze tabeli
Kryteria: ,
R.5.5 Skrypt 1 wypełnia tabelę zwróconymi zapytaniem wierszami, odpowiednie pola w kolumnach, wyświetlone są wszystkie zwrócone wiersze
R.2.8 W sekcji lewej umieszczona tabela o 3 kolumnach (w skrypcie lub HTML) zastosowane znaczniki <table>, <tr>, <td>
ok. 3 min
Polecenie z arkusza
Wszystkie zwrócone zapytaniem wiersze wyświetla jako kolejne wiersze tabeli zgodnie z ilustracją 2
Pętla foreach ($staff as $employee): ... endforeach; w <tbody>: jeden <tr> na osobę, trzy <td> w kolejności kolumn (R.5.5, R.2.8). Podgląd jak lewa sekcja ilustracji 2: Joanna Nosowska, Karolina Mizera, Leon Zawodowiec i dalej - 7 ratowników. Po wybraniu „Policjant” albo przy pierwszym wejściu tabela ma 11 wierszy.
Zmienna pętli nazywa się $employee (pracownik), a nie $row - w szablonie od razu widać, co jest w środku.
index.phpPHPzmienione: 7 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"checked> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody><?phpforeach ($staffas$employee): ?> <tr> <td><?= escape($employee['id']) ?></td> <td><?= escape($employee['imie']) ?></td> <td><?= escape($employee['nazwisko']) ?></td> </tr><?phpendforeach; ?> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 44: Skrypt 1: zaznaczona opcja zostaje po wysłaniu
ok. 2 min
Polecenie z arkusza
Pole opcji z treścią „Policjant”, domyślnie zaznaczone
Po kliknięciu „Pokaż” strona ładuje się od nowa i zwykłe checked wróciłoby na „Policjant” - nad tabelą ratowników. Na ilustracji 2 widać właśnie taki stan, bo tak działa najprostsza wersja. Tu checked dostaje pole, którego wartość jest w $option: przy pierwszym wejściu to „policjant” (domyślne zaznaczenie z arkusza i R.3.5), po wybraniu ratowników - „ratownik”. Użytkownik zawsze widzi, co wybrał.
index.phpPHPzmienione: 2 linie
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"<?= $option === 'policjant' ? ' checked' : ''?>> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"<?= $option === 'ratownik' ? ' checked' : ''?>> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody><?phpforeach ($staffas$employee): ?> <tr> <td><?= escape($employee['id']) ?></td> <td><?= escape($employee['imie']) ?></td> <td><?= escape($employee['nazwisko']) ?></td> </tr><?phpendforeach; ?> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 45: Skrypt 2: zapytanie 3
Kryteria:
R.5.2 Przynajmniej jeden skrypt zawiera instrukcję wysyłającą do bazy danych zapytanie
ok. 2 min
Polecenie z arkusza
Skrypt 2: wysyła do bazy danych zapytanie 3
Zapytanie 3 z kwerendy.txt (NOT EXISTS) przez $pdo->query(...) - bez wartości od użytkownika, więc bez prepare (R.5.2). Wynik od razu w tablicy $withoutReports; nazwa mówi, co jest w środku.
index.phpPHPzmienione: 6 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Skrypt 2: osoby bez żadnego zgłoszenia (zapytanie 3)$withoutReports = $pdo->query('SELECT p.id, p.nazwisko FROM personel AS p WHERE NOT EXISTS (SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id)')->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"<?= $option === 'policjant' ? ' checked' : ''?>> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"<?= $option === 'ratownik' ? ' checked' : ''?>> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody><?phpforeach ($staffas$employee): ?> <tr> <td><?= escape($employee['id']) ?></td> <td><?= escape($employee['imie']) ?></td> <td><?= escape($employee['nazwisko']) ?></td> </tr><?phpendforeach; ?> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
R.5.6 Skrypt 2 wyświetla wszystkie zwrócone zapytaniem 3 wiersze jako elementy <li> (id i nazwisko)
R.2.7 W sekcji prawej umieszczona jedna lista numerowana z elementami, zastosowane znaczniki <ol>, <li> (w skrypcie lub HTML)
ok. 2 min
Polecenie z arkusza
Wszystkie zwrócone zapytaniem wiersze wyświetla jako elementy listy zgodnie z ilustracją 2
Jeden <li> na osobę: identyfikator, spacja, nazwisko (R.5.6, R.2.7). Podgląd: 2 Nowak, 3 Nosowska, 4 Mizera - jak na ilustracji 2. To identyfikatory, które użytkownik wpisze w pole pod listą.
index.phpPHPzmienione: 3 linie
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Skrypt 2: osoby bez żadnego zgłoszenia (zapytanie 3)$withoutReports = $pdo->query('SELECT p.id, p.nazwisko FROM personel AS p WHERE NOT EXISTS (SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id)')->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"<?= $option === 'policjant' ? ' checked' : ''?>> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"<?= $option === 'ratownik' ? ' checked' : ''?>> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody><?phpforeach ($staffas$employee): ?> <tr> <td><?= escape($employee['id']) ?></td> <td><?= escape($employee['imie']) ?></td> <td><?= escape($employee['nazwisko']) ?></td> </tr><?phpendforeach; ?> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol><?phpforeach ($withoutReportsas$person): ?> <li><?= escape($person['id']) ?><?= escape($person['nazwisko']) ?></li><?phpendforeach; ?> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 47: Skrypt 3: zapis zgłoszenia przed odczytami
Kryteria:
R.5.7 Skrypt 3 wysyła do bazy zmodyfikowane zapytanie 4 tak, że we wstawianym rekordzie id_personelu ma wartość przesłaną z formularza (sprawdzić w phpMyAdmin )
ok. 4 min
Polecenie z arkusza
Jeżeli wysłano dane z formularza to skrypt wysyła do bazy danych zmodyfikowane zapytanie 4 tak, że w miejscu id personelu jest podana wartość wysłana z formularza
Skrypt 3 stoi w kodzie przed Skryptem 1 i 2, choć arkusz wymienia go ostatni. PHP wykonuje plik od góry: gdy zapis jest pierwszy, lista osób bez zgłoszeń czyta bazę już po zapisie. W wersji podstawowej z egzaminu po dodaniu zgłoszenia dla osoby 2 lista nadal pokazywała „2 Nowak” - aż do następnego wejścia.
isset($_POST['person']) - „jeżeli wysłano dane z formularza”: pole person istnieje tylko po kliknięciu „Dodaj zgłoszenie”.
filter_var(..., FILTER_VALIDATE_INT, ...) - liczba całkowita od 1 albo false (pusty napis, „abc”, „2 OR 1=1”); przy błędzie komunikat zamiast zapisu.
Zapytanie 4 przygotowane, z parametrem :person w miejscu identyfikatora osoby; pojazd 14 i CURDATE() zostają (R.5.7).
Sprawdzone w bazie, jak każe klucz: po wpisaniu 2 w tabeli rejestr przybył rekord 31 (osoba 2, pojazd 14, data dzisiejsza), a lista od razu pokazuje tylko Nosowską i Mizerę.
index.phpPHPzmienione: 15 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 3: nowe zgłoszenie z drugiego formularza (zapytanie 4) - przed odczytami$message = '';if (isset($_POST['person'])) {$personId = filter_var($_POST['person'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);if ($personId === false) {$message = 'Podaj identyfikator osoby - liczbę całkowitą większą od zera.'; } else {$statement = $pdo->prepare('INSERT INTO rejestr (data, id_personel, id_pojazd) VALUES (CURDATE(), :person, 14)');$statement->execute(['person' => $personId]); }}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Skrypt 2: osoby bez żadnego zgłoszenia (zapytanie 3)$withoutReports = $pdo->query('SELECT p.id, p.nazwisko FROM personel AS p WHERE NOT EXISTS (SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id)')->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"<?= $option === 'policjant' ? ' checked' : ''?>> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"<?= $option === 'ratownik' ? ' checked' : ''?>> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody><?phpforeach ($staffas$employee): ?> <tr> <td><?= escape($employee['id']) ?></td> <td><?= escape($employee['imie']) ?></td> <td><?= escape($employee['nazwisko']) ?></td> </tr><?phpendforeach; ?> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol><?phpforeach ($withoutReportsas$person): ?> <li><?= escape($person['id']) ?><?= escape($person['nazwisko']) ?></li><?phpendforeach; ?> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form><?phpif ($message !== ''): ?> <p><?= escape($message) ?></p><?phpendif; ?> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 48: Skrypt 3: przekierowanie po zapisie i błąd klucza obcego
ok. 5 min
Polecenie z arkusza
Jeżeli wysłano dane z formularza to skrypt wysyła do bazy danych zmodyfikowane zapytanie 4 tak, że w miejscu id personelu jest podana wartość wysłana z formularza
Dwa przypadki, których wersja z egzaminu nie obsługuje:
PRG (Post/Redirect/Get). Po udanym zapisie header('Location: index.php?added=2', true, 303) i exit: przeglądarka pobiera stronę od nowa zwykłym GET-em. Odświeżenie (F5) nie wyśle formularza drugi raz, więc nie powstanie zdublowane zgłoszenie. Kod 303 („zobacz inny adres”) mówi wprost: po POST przejdź GET-em. Komunikat „Dodano zgłoszenie…” niesie parametr added w adresie, sprawdzany filter_var jak każde wejście. Przed exit zamknięcie połączenia - to nadal ostatnia operacja na bazie.
Osoba spoza bazy. Identyfikator 99 łamie klucz obcy i baza zgłasza błąd o kodzie SQLSTATE 23000. catch (PDOException ...) zamienia go na komunikat „Nie ma osoby o identyfikatorze 99.”; każdy inny błąd jest rzucany dalej (throw), bo udawanie, że nic się nie stało, ukryłoby awarię.
Podgląd pokazuje stronę po przekierowaniu: komunikat pod formularzem i lista bez Nowaka.
Typowe błędy
header() musi stać przed jakimkolwiek wypisanym znakiem - nawet spacją przed <?php albo BOM-em w pliku. Dlatego cała logika jest nad <!DOCTYPE html>.
index.phpPHPzmienione: 16 linii
<?php// Połączenie z bazą: błędy jako wyjątki, wiersze jako tablice z nazwami kolumntry {$pdo = new PDO('mysql:host=localhost;dbname=zgloszenia;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);} catch (PDOException $exception) {http_response_code(500);exit('Nie udało się połączyć z bazą danych. Spróbuj ponownie za chwilę.');}functionescape(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');}// Skrypt 3: nowe zgłoszenie z drugiego formularza (zapytanie 4) - przed odczytami$message = '';$addedId = filter_var($_GET['added'] ?? '', FILTER_VALIDATE_INT);if ($addedId !== false) {$message = "Dodano zgłoszenie dla osoby o identyfikatorze $addedId.";}if (isset($_POST['person'])) {$personId = filter_var($_POST['person'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);if ($personId === false) {$message = 'Podaj identyfikator osoby - liczbę całkowitą większą od zera.'; } else {$statement = $pdo->prepare('INSERT INTO rejestr (data, id_personel, id_pojazd) VALUES (CURDATE(), :person, 14)');try {$statement->execute(['person' => $personId]);$statement = null;$pdo = null;header('Location: index.php?added=' . $personId, true, 303);exit; } catch (PDOException $exception) {if ($exception->getCode() !== '23000') {throw$exception; }$message = "Nie ma osoby o identyfikatorze $personId."; } }}// Skrypt 1: personel o statusie wybranym w formularzu (zapytanie 1)$allowedOptions = ['policjant', 'ratownik'];$option = $_POST['option'] ?? 'policjant';if (!in_array($option, $allowedOptions, true)) {$option = 'policjant';}$statement = $pdo->prepare('SELECT id, imie, nazwisko FROM personel WHERE status = :status');$statement->execute(['status' => $option]);$staff = $statement->fetchAll();// Skrypt 2: osoby bez żadnego zgłoszenia (zapytanie 3)$withoutReports = $pdo->query('SELECT p.id, p.nazwisko FROM personel AS p WHERE NOT EXISTS (SELECT 1 FROM rejestr AS r WHERE r.id_personel = p.id)')->fetchAll();// Wszystkie dane pobrane - zamknięcie połączenia$statement = null;$pdo = null;?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>ZGŁOSZENIA</title> <linkrel="stylesheet"href="styl.css"></head><body> <header> <h1>Zgłoszenia wydarzeń</h1> </header> <main> <sectionid="staff"> <h2>Personel</h2> <formmethod="post"> <inputtype="radio"id="police"name="option"value="policjant"<?= $option === 'policjant' ? ' checked' : ''?>> <labelfor="police">Policjant</label> <inputtype="radio"id="rescuer"name="option"value="ratownik"<?= $option === 'ratownik' ? ' checked' : ''?>> <labelfor="rescuer">Ratownik</label> <buttontype="submit">Pokaż</button> </form> <h3>Wybrano opcję: <?= escape($option) ?></h3> <table> <thead> <tr> <th>Id</th> <th>Imię</th> <th>Nazwisko</th> </tr> </thead> <tbody><?phpforeach ($staffas$employee): ?> <tr> <td><?= escape($employee['id']) ?></td> <td><?= escape($employee['imie']) ?></td> <td><?= escape($employee['nazwisko']) ?></td> </tr><?phpendforeach; ?> </tbody> </table> </section> <sectionid="new-report"> <h2>Nowe zgłoszenie</h2> <ol><?phpforeach ($withoutReportsas$person): ?> <li><?= escape($person['id']) ?><?= escape($person['nazwisko']) ?></li><?phpendforeach; ?> </ol> <formmethod="post"> <labelfor="person">Wybierz id osoby z listy: </label> <inputtype="number"id="person"name="person"min="1"step="1"required> <buttontype="submit">Dodaj zgłoszenie</button> </form><?phpif ($message !== ''): ?> <p><?= escape($message) ?></p><?phpendif; ?> </section> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (index.php)
Krok 49: Nazwy zmiennych
Kryteria:
R.5.9 W skryptach zastosowana przynajmniej jedna zmienna oraz nazwy wszystkich zmiennych są w języku polskim lub angielskim i odzwierciedlają cel zastosowania
ok. 1 min
Polecenie z arkusza
Należy stosować znaczące nazewnictwo zmiennych i funkcji w języku polskim lub angielskim
Każda zmienna mówi, co przechowuje (R.5.9): $pdo (połączenie), $allowedOptions, $option, $staff i $employee (personel i jeden pracownik), $withoutReports i $person (osoby bez zgłoszeń), $personId, $addedId, $message, $statement, $exception. Jedna nazwa na jedno znaczenie - w wersji podstawowej $person był raz wierszem tabeli, a raz identyfikatorem z formularza.
Kroki 50-51 · po tym etapie masz 39 z 39 kryteriów (pkt)
Krok 50: Co jest inaczej i dlaczego
ok. 3 min
Polecenie z arkusza
Ocenie będzie podlegać 5 rezultatów: operacje na bazie danych, zawartość witryny internetowej, działanie witryny internetowej, styl CSS witryny internetowej oraz skrypt połączenia z bazą.
Obie wersje dają te same punkty. Różnice:
wersja podstawowa
ta wersja
układ
display: flex na <main>, width: 50%
display: grid, grid-template-columns: 50% 50%
tabela
wiersze w <table>
<thead> i <tbody>
baza
mysqli_* z tabeli 1
PDO, wyjątki, FETCH_ASSOC
opcja z formularza
wstawiona do SQL
biała lista + parametr :status
zapytanie 3
NOT IN
NOT EXISTS
zapis zgłoszenia
po wypisaniu listy, bez kontroli
przed odczytami, filter_var, błąd 23000 jako komunikat
po zapisie
ta sama strona (F5 dubluje)
przekierowanie 303 (PRG)
Czego tu jeszcze nie ma, a w prawdziwym systemie zgłoszeń by było: token CSRF w formularzu zapisu (losowa wartość z sesji, sprawdzana przy zapisie - blokuje dodanie zgłoszenia z cudzej strony) i logowanie dyspozytora.
Czas przeznaczony na wykonanie zadania wynosi 150 minut.
Wersja podstawowa, gdy liczy się czas albo PDO nie jest opanowane: funkcje mysqli_* i isset są w tabeli 1, a skrypty stoją tam, gdzie widać ich efekt.
Ta wersja, gdy PDO, filter_var i przekierowanie są pisane z pamięci. Przekierowanie ma jedną pułapkę: pusta linia albo spacja przed <?php blokuje header() - wtedy zapis się wykona, ale strona zostanie pusta.
Mieszanie też daje pełne punkty: siatka z tej wersji i mysqli_* z podstawowej.
Krok 52 · po tym etapie masz 39 z 39 kryteriów (pkt)
Krok 52: Plik przeglądarka.txt i nagranie płyty
ok. 5 min
Polecenie z arkusza
Po zakończeniu pracy utwórz plik tekstowy o nazwie przeglądarka.txt. Zapisz w nim nazwę przeglądarki internetowej, w której weryfikowana była poprawność działania witryny. Umieść go w folderze z numerem zdającego. Nagraj płytę z rezultatami pracy.
W przeglądarka.txt nazwa przeglądarki, w której strona była sprawdzana.
W folderze z numerem zdającego: import.png, index.php, kw1.jpg - kw4.jpg, kwerendy.txt, przeglądarka.txt, styl.css. Po nagraniu sprawdź odczyt płyty.
Symulacja ekranu w przeglądarce - w Android Studio / Visual Studio wygląd może się nieco różnić.
To jedno z możliwych rozwiązań. Egzaminator ocenia spełnienie kryteriów z klucza odpowiedzi, a nie zgodność z tym kodem - inna, poprawna droga też daje punkty.
Autor rozwiązania: Bartosz Bryniarski. Licencja CC BY-NC 4.0 - kopiowanie i przeróbki do celów niekomercyjnych, z podaniem autora i źródła (link do tej strony) (zasady wykorzystania). Treść arkusza i klucza oceniania pochodzi z CKE.