INF.03-05-23.01-SGStyczeń 2023 · 32 kroki · ok. 66 min · 34 kryteria z klucza
Rozwiązanie krok po kroku: Internetowy kalendarz sierpniowy (Nowoczesny kod (grid, flex, PDO))
To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza kalendarz z czterema zapytaniami, logo 120 × 120 px i strona organizer.php z formularzem zapisującym wydarzenie i kalendarzem sierpnia - i tak samo spełnia każde kryterium klucza. Inna jest droga:
jeden <header> z siatką zamiast trzech pływających banerów: szerokości 40%, 40% i 20% opisuje reguła grid-template-columns,
bloki kalendarza we flexboksie z zawijaniem - układają się w rzędy jak przy float, ale stopka nie potrzebuje clear; kolory w zmiennych CSS,
PHP na PDO: cały kod rozmawiający z bazą stoi na górze pliku organizer.php - najpierw zapis z formularza, potem odczyt kalendarza,
zapis zapytaniem przygotowanym (wpis z apostrofem, np. „Urodziny Ani's”, nie psuje zapytania) i przekierowanie po zapisie (wzorzec PRG): odświeżenie strony po wysłaniu nie wysyła formularza drugi raz,
bezpieczne wypisywanie wpisów (htmlspecialchars w funkcji e()) i komunikat, gdy bazy nie da się odczytać.
Na końcu krótkie porównanie z wersją podstawową (zakładka obok). Baza i logo pochodzą z prawdziwego archiwum zad5, a podgląd od kroków z PHP to wynik prawdziwego uruchomienia kodu w PHP 8.2 - od kroków formularza po wysłaniu wpisu „zakupy”, jak na obrazie 2.
Jak liczą się punkty. Klucz ma 34 kryteria (R.1.1 - R.5.7), każde za 1 punkt. Próg zdania to 75%, czyli 26 punktów. Przy każdym kroku jest napisane, które kryteria spełnia.
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 34 kryteriów (pkt)
Krok 1: Baza danych kalendarz
ok. 2 min
Polecenie z arkusza
Utwórz bazę danych o nazwie kalendarz
Rozpakuj zad5.zip z pulpitu (hasło kaL(end)arz% - z nawiasami w środku i procentem na końcu) do folderu z numerem zdającego: baza5.sql, logo1.png i logo2.png. W phpMyAdmin Nowa → kalendarz albo zapytaniem:
CREATE DATABASE kalendarz CHARACTER SET utf8mb4 COLLATE utf8mb4_polish_ci;
Arkusz nie podaje porównywania. Tabela z baza5.sql ma kodowanie zapisane we własnej definicji (latin1), więc ustawienie bazy jej nie zmienia - wraca to przy zapisie z formularza.
Typowe błędy
Nazwa bazy musi brzmieć dokładnie kalendarz (nie baza5) - skrypt łączy się z bazą po nazwie (R.5.1).
Krok 2: Import tabeli z pliku baza5.sql
ok. 2 min
Polecenie z arkusza
Do bazy kalendarz zaimportuj tabele z pliku baza5.sql z rozpakowanego archiwum
Zaznaczona baza kalendarz, Import → baza5.sql → Wykonaj. Tabela zadania z obrazu 1: 63 dni - lipiec i sierpień 2020 oraz jeden dzień listopada. Pole miesiac ma nazwę bez polskich liter (sierpien), a dni bez zadań mają w polu wpis pusty tekst.
Krok 3: Zrzut ekranu import.png
Kryteria:
R.1.1 Wykonano import tabel do bazy danych kalendarz czynność udokumentowano plikiem o nazwie import w formacie PNG
ok. 2 min
Polecenie z arkusza
Wykonaj zrzut ekranu po imporcie. Zrzut zapisz w formacie PNG pod nazwą import. Nie kadruj zrzutu. Zrzut powinien obejmować cały ekran monitora, z widocznym paskiem zadań. Na zrzucie powinny być widoczne elementy wskazujące na poprawnie wykonany import tabel
Cały ekran z paskiem zadań, w PNG, pod nazwą import. Zrzuty zapytań kw1 - kw4 będą już 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: zadania z sierpnia
Kryteria: ,
R.1.2 Zapisano plik kwerendy.txt zawierający co najmniej jedno zapytanie SQL, wynikające z treści zadania
R.1.3 Utworzono zapytanie 1 wybierające jedynie pola dataZadania i wpis dla zadań z sierpnia. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT dataZadania, wpis FROM zadania WHERE miesiac = 'sierpien'; (poprawne też: dataZadania LIKE '____-08-__' lub użycie %) oraz wynik działania udokumentowano zrzutem, na którym widoczny jest w komunikacie zapis o liczbie rekordów "(31 ogółem...)", rekordy są jedynie z sierpnia, jedynie pola dataZadania i wpis. Na zrzucie nie muszą być widoczne wszystkie zwrócone zapytaniem wiersze
ok. 2 min
Polecenie z arkusza
Zapytanie 1: wybierające jedynie pola dataZadania i wpis dla zadań, z miesiąca sierpnia
WHERE miesiac = 'sierpien' - w bazie bez polskich liter. ORDER BY dataZadania ustala kolejność dni: bez sortowania serwer może oddać wiersze w dowolnej kolejności, a kalendarz musi iść od 1 do 31 sierpnia. Wynik: 31 wierszy („31 ogółem”), od 2020-08-01 Remont. To samo zapytanie wyśle Skrypt 1. Zapytanie do kwerendy.txt, zrzut kw1.jpg.
Krok 5: Zapytanie 2: miesiąc i rok dla 7 sierpnia 2020
Kryteria:
R.1.4 Utworzono zapytanie 2 wybierające jedynie pola miesiac i rok dla zadania z dnia 7 sierpnia 2020 r. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT miesiac, rok FROM zadania WHERE dataZadania = '2020-08- 07'; oraz wynik działania udokumentowano zrzutem, na którym widoczne jest: sierpien, 2020
ok. 1 min
Polecenie z arkusza
Zapytanie 2: wybierające jedynie pola miesiac i rok dla zadania z dnia 7 sierpnia 2020 r.
Data w SQL w postaci rok-miesiąc-dzień, w apostrofach. Wynik: sierpien, 2020 - jak w kluczu. Zrzut: kw2.jpg.
Krok 6: Zapytanie 3: wizyty u weterynarza w każdym miesiącu
Kryteria:
R.1.5 Utworzono zapytanie 3 wybierające jedynie nazwę miesiąca oraz w kolumnie wpis zliczające dla każdego miesiąca wpisy rozpoczynające się od słowa „Weterynarz”. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT miesiac, COUNT(*) FROM zadania WHERE wpis LIKE 'Weterynarz%' GROUP BY miesiac; (zamiast * może być dowolne pole) oraz wynik działania udokumentowano zrzutem, na którym widoczne jest jedynie: lipiec, 2; sierpien, 2
ok. 2 min
Polecenie z arkusza
Zapytanie 3: wybierające jedynie nazwę miesiąca oraz liczące dla każdego miesiąca ile jest wpisów rozpoczynających się od słowa „Weterynarz” w kolumnie wpis
LIKE 'Weterynarz%' - % tylko na końcu wzorca znaczy „zaczyna się od”,
GROUP BY miesiac - liczenie osobno dla każdego miesiąca; COUNT(id) z aliasem wizyty - kolumna wyniku ma nazwę zamiast COUNT(*) (klucz dopuszcza dowolne pole),
ORDER BY miesiac - stała kolejność grup.
Wynik: lipiec 2 i sierpien 2 - jak w kluczu. Zrzut: kw3.jpg.
kwerendy.txtTekstzmienione: 7 linii
-- Zapytanie 1SELECT dataZadania, wpisFROM zadaniaWHERE miesiac = 'sierpien'ORDER BY dataZadania;-- Zapytanie 2SELECT miesiac, rokFROM zadaniaWHERE dataZadania = '2020-08-07';-- Zapytanie 3SELECT miesiac, COUNT(id) AS wizytyFROM zadaniaWHERE wpis LIKE 'Weterynarz%'GROUP BY miesiacORDER BY miesiac;
Podgląd strony po tym kroku
Krok 7: Zapytanie 4: nowy wpis na 9 sierpnia 2020
Kryteria:
R.1.6 Utworzono zapytanie 4 aktualizujące pole wpis dla zadania z datą 2020-08-09, nowy wpis to „Wycieczka: Zalew Mietkowski”. W pliku z kwerendami lub na zrzucie istnieje zapis np.: UPDATE zadania SET wpis='Wycieczka: Zalew Mietkowski' WHERE dataZadania="2020-08-09"; oraz wynik działania udokumentowano zrzutem, na którym widoczny jest komunikat o poprawnej aktualizacji lub tabela ze zaktualizowanym wpisem
ok. 2 min
Polecenie z arkusza
Zapytanie 4: aktualizujące pole wpis dla zadania z datą 9 sierpnia 2020, nowy wpis to „Wycieczka: Zalew Mietkowski”
UPDATE z warunkiem na dacie - zmienia się tylko jeden rekord (bez WHERE zmieniłyby się wszystkie 63). Zrzut kw4.jpg: komunikat o jednym zmienionym wierszu i rekord po zmianie. To zapytanie wróci w skrypcie formularza - z parametrem w miejscu tekstu.
kwerendy.txtTekstzmienione: 5 linii
-- Zapytanie 1SELECT dataZadania, wpisFROM zadaniaWHERE miesiac = 'sierpien'ORDER BY dataZadania;-- Zapytanie 2SELECT miesiac, rokFROM zadaniaWHERE dataZadania = '2020-08-07';-- Zapytanie 3SELECT miesiac, COUNT(id) AS wizytyFROM zadaniaWHERE wpis LIKE 'Weterynarz%'GROUP BY miesiacORDER BY miesiac;-- Zapytanie 4UPDATE zadaniaSET wpis = 'Wycieczka: Zalew Mietkowski'WHERE dataZadania = '2020-08-09';
Podgląd strony po tym kroku
Etap 2: Grafika i strona HTML
Kroki 8-16 · po tym etapie masz 17 z 34 kryteriów (pkt)
Krok 8: Logo 120 × 120 px
Kryteria:
R.2.1 Plik logo2.png przeskalowano do wymiarów 120 px na 120 px (± 2 px)
ok. 2 min
Polecenie z arkusza
Plik logo2.png, wypakowany z archiwum, należy przeskalować dokładnie do wymiarów 120 px na 120 px
Grafika jest taka sama jak w wersji podstawowej: w GIMP-ie Obraz → Skaluj obraz z 300 × 300 do 120 × 120 i Nadpisz logo2.png - PNG zachowuje przezroczyste tło.
Typowe błędy
Sam atrybut width w HTML-u nie zmienia pliku - klucz sprawdza wymiary pliku logo2.png.
R.2.2 Witrynę internetową zapisano w pliku organizer.php oraz zapisano jawnie standard kodowania polskich znaków i deklarację HTML5 <!DOCTYPE HTML> i zadeklarowano język witryny: polski np. <html lang="pl">
ok. 2 min
Polecenie z arkusza
Składa się ze strony o nazwie organizer.php. Zapisana w języku HTML 5. Jawnie zastosowany właściwy standard kodowania polskich znaków. Zadeklarowany język witryny: polski.
<!DOCTYPE html>, <html lang="pl"> i <meta charset="UTF-8"> jako pierwszy element <head> (R.2.2). Rozszerzenie .php - strona działa przez http://localhost/....
R.3.4 Strona zawiera działające połączenie z zewnętrznym arkuszem stylów o nazwie styl5.css , formatowanie pochodzi jedynie z tego arkusza Uwaga: kryterium to nie jest spełnione również, gdy zdający źle przypisał co najmniej jeden selektor do bloku (np. kropka zamiast #)
ok. 1 min
Polecenie z arkusza
Arkusz stylów w pliku o nazwie styl5.css prawidłowo połączony z kodem strony
Nazwa z cyfrą 5: styl5.css. Całe formatowanie pochodzi z tego pliku (R.3.4).
Typowe błędy
Klucz odbiera R.3.4 także wtedy, gdy choć jeden selektor nie trafia w blok (np. kropka zamiast #).
Krok 12: Bloki: baner z trzech części, blok główny, stopka
Kryteria:
R.2.4 Układ strony zdefiniowano dzieląc ją na: 3 bloki banera, blok główny i stopkę. Zastosowano znaczniki sekcji
ok. 2 min
Polecenie z arkusza
Podział strony na bloki: trzy bloki banera, poniżej blok główny, w którym znajdują się generowane skryptem bloki kalendarza, na dole blok stopki. Podział zrealizowany za pomocą znaczników sekcji tak, aby po uruchomieniu w przeglądarce wygląd układu bloków był zgodny z Obrazem 2
Baner strony jest jeden, więc to jeden <header>, a w nim trzy bloki banera - trzy <section> z identyfikatorami mówiącymi, co zawierają: title, add-event, logo. <header> będzie dla nich kontenerem nadrzędnym - klucz przy R.4.4 wprost przewiduje takie rozwiązanie. Blok główny to <main> (bloki kalendarza wygeneruje w nim skrypt), stopka - <footer> (R.2.4).
Krok 14: Drugi blok banera: formularz POST do tej samej strony
Kryteria: , , ,
R.2.6 W drugim bloku banera umieszczono formularz, zastosowano znacznik <form>
R.2.7 Na stronie umieszczono pole do wprowadzania danych oraz przycisk "OK"
R.3.2 Przycisk ma funkcję wysyłania danych (input lub button z typem submit lub button bez typu)
R.3.3 Zdefiniowano dla formularza metodę POST
ok. 3 min
Polecenie z arkusza
Zawartość drugiego bloku banera: formularz wysyłający dane metodą bezpieczną do tej samej strony, zawierający: Pole edycyjne podpisane: „Zapisz wydarzenie: ”, Przycisk o treści „OK” wysyłający zawartość formularza
method="post" - „metodę bezpieczną” arkusz rozumie jako POST, a klucz (R.3.3) wymaga POST; tu to także właściwy wybór, bo formularz zmienia dane w bazie. Bez atrybutu action formularz wysyła dane do tej samej strony,
<label for="event"> - podpis połączony z polem; name="event" - nazwa, pod którą tekst trafi do $_POST; required - przeglądarka nie wyśle pustego pola, więc kliknięcie OK bez tekstu nie skasuje wpisu,
R.2.5 W banerze pierwszym zapisano nagłówek h1: "Organizer: SIERPIEŃ" oraz w stopce zapisano paragraf
ok. 1 min
Polecenie z arkusza
Zawartość stopki: Paragraf (akapit) o treści: „Stronę wykonał: ”, dalej wstawiony numer zdającego
Akapit w <footer>. Na egzaminie wpisujesz swój numer zdającego (PESEL), tu stoi autor rozwiązania. Z nagłówkiem <h1> to R.2.5. Blok główny wypełni Skrypt 1.
Kroki 17-22 · po tym etapie masz 27 z 34 kryteriów (pkt)
Krok 17: Plik styl5.css: zmienne i krój czcionki
Kryteria:
R.4.1 Ustawiono domyślne formatowanie wszystkich selektorów (*): krój czcionki Tahoma
ok. 2 min
Polecenie z arkusza
Ustawione domyślne wartości dla wszystkich selektorów stylu CSS: krój czcionki Tahoma
#7DA453 pojawia się w arkuszu cztery razy (banery, stopka, ramka dnia, dzień pod kursorem), więc trafia do zmiennej--green. Selektor * z Tahomą i rodziną zapasową sans-serif (R.4.1).
R.4.4 Ustawiono szerokość bloków banera: 40% pierwszy i drugi oraz 20% trzeci (width lub grid- template-columns lub flex) poprawne również rozwiązanie bez szerokości dla trzeciego bloku jeśli bloki wstawiono w kontener nadrzędny
R.4.5 Ustawiono wysokość 120px dla trzech bloków banera
ok. 2 min
Polecenie z arkusza
Dla wszystkich bloków banera: kolor tła #7DA453, biały kolor czcionki, wysokość 120 px. Dodatkowo pierwszy i drugi blok banera ma szerokość 40%, trzeci 20%
display: grid na <header> i grid-template-columns: 40% 40% 20% - trzy kolumny o szerokościach z arkusza; bloki banera nie mają własnej szerokości (klucz przy R.4.4 przyjmuje szerokości w grid-template-columns),
header > section - selektor dziecka: trafia w trzy bloki banera i tylko w nie, bez klasy w HTML-u. Kolor, biała czcionka i wysokość 120 px (tyle ma logo).
Typowe błędy
width: 40% dopisane jeszcze na blokach liczy się w siatce od kolumny - blok miałby 40% z 40% strony.
R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z obrazem 2 w arkuszu (Uwaga! Blok główny nie musi zawierać bloków kalendarza; prawidłowo zastosowano właściwości CSS układające bloki na stronie, np. float i clear albo display flex albo grid)
R.4.8 Ustawiono dla bloku kalendarza: kolor tła #AED581, szerokość 200px, wysokość 100px, marginesy zewnętrzne (margin) 7px, obramowanie 1px solid #7DA453 (np. za pomocą klasy. Użycie id jest błędem)
ok. 3 min
Polecenie z arkusza
Dla bloku kalendarza: kolor tła #AED581, szerokość 200 px, wysokość 100 px, marginesy zewnętrzne 7 px, obramowanie o szerokości 1 px, linią ciągłą o kolorze #7DA453. Podział zrealizowany za pomocą znaczników sekcji tak, aby po uruchomieniu w przeglądarce wygląd układu bloków był zgodny z Obrazem 2
main { display: flex; flex-wrap: wrap; } - bloki kalendarza stają w rzędzie, a gdy się nie mieszczą, przechodzą do następnego. To typowy układ „kafelków” o stałej szerokości; siatka byłaby tu potrzebna dopiero przy kolumnach dopasowanych do okna,
klasa .day - bloków będzie 31, a klucz przy R.4.8 pisze wprost, że identyfikator jest tu błędem. Wymiary, margines i ramka z arkusza; ramka w kolorze ze zmiennej.
Stopka stoi pod <main> sama z siebie - bez clear (R.3.1).
R.4.6 Ustawiono marginesy wewnętrzne (padding) 5px dla stopki oraz wewnętrzny górny (padding-top) 50px dla selektora form
R.4.7 Ustawiono wyrównanie tekstu do środka dla selektora h5
ok. 1 min
Polecenie z arkusza
Dla selektora nagłówka piątego stopnia: wyrównanie tekstu do środka. Dla selektora formularza: margines wewnętrzny górny: 50 px
Selektory znaczników h5 i form - uwaga pod listą stylów wyklucza tu klasy, a klucz (R.4.6, R.4.7) uznaje tylko taki zapis. padding-top zsuwa formularz mniej więcej na środek wysokości banera, jak na obrazie 2.
Typowe błędy
margin-top zamiast padding-top przy form - klucz uznaje tylko margines wewnętrzny.
Kroki 23-30 · po tym etapie masz 34 z 34 kryteriów (pkt)
Krok 23: Połączenie z bazą na górze pliku i zamknięcie
Kryteria: ,
R.5.1 Skrypt zawiera działające połączenie z serwerem bazy danych i wybór bazy kalendarz
R.5.2 Ostatnią operacją na bazie jest jej zamknięcie (zgodne ze składnią)
ok. 4 min
Polecenie z arkusza
Napisany w języku PHP, w pliku organizer.php. Skrypt łączy się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie kalendarz. Na końcu działania skrypt zamyka połączenie z serwerem.
Oba skrypty z arkusza - zapis z formularza i Skrypt 1 rysujący kalendarz - stoją w jednym bloku PHP na górze organizer.php, przed HTML-em. Kolejność jest ta sama co w wersji podstawowej: najpierw zapis, potem odczyt, więc nowy wpis jest od razu w kalendarzu.
new PDO(...) - serwer localhost, baza kalendarz, użytkownik root bez hasła (R.5.1); ERRMODE_EXCEPTION - błąd bazy rzuca wyjątek, FETCH_ASSOC - wiersze z nazwami kolumn,
$pdo = null - PDO zamyka połączenie, gdy znika odwołanie do obiektu; zostanie ostatnią operacją na bazie (R.5.2),
try / catch (PDOException ...) - przy awarii bazy strona pokaże zwykłe zdanie, a nie szczegóły serwera.
PDO zamiast mysqli z tabeli 1 - klucz przyjmuje obie biblioteki (uwaga przy R.5). Od tego kroku podgląd to wynik prawdziwego uruchomienia strony.
organizer.phpPHPzmienione: 12 linii
<?phptry {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (organizer.php)
Krok 24: Formularz: tekst z pola edycyjnego
Kryteria:
R.5.6 W skrypcie obsługującym formularz zastosowano tablicę $_POST, zapis jest poprawny składniowo i logicznie
ok. 2 min
Polecenie z arkusza
Działanie skryptu obsługującego formularz: Skrypt pobiera dane z pola edycyjnego
Tekst jest w tablicy $_POST pod nazwą z atrybutu name (R.5.6). Przy pierwszym wejściu na stronę formularz nie był wysłany i klucza event nie ma: operator ?? podstawia wtedy pusty tekst - bez ostrzeżenia „Undefined array key”. trim obcina spacje z brzegów, więc wpis z samych spacji też będzie pusty. Od tego kroku podgląd to strona po wysłaniu formularza z wpisem „zakupy”.
R.5.7 Skrypt obsługujący formularz uaktualnia dane w bazie danych. Dane pobrane z formularza są wpisane do rekordu o dacie 2020-08-09. Należy sprawdzić w phpMyAdmin czy po wciśnięciu przycisku baza została uaktualniona, nie jest wymagane uaktualnienie na stronie
ok. 3 min
Polecenie z arkusza
Następnie za pomocą zmodyfikowanego zapytania 4 uaktualnia dane w bazie danych tak, że dane w polu wpis zostały pobrane z formularza. Aby uprościć zadanie skrypt wprowadza wpis zawsze do rekordu o dacie 2020-08-09.
Zapytanie przygotowane: w zapytaniu 4 w miejscu tekstu stoi parametr :entry, a data - parametr :day; wartości podaje execute([...]). Baza dostaje treść zapytania i dane osobno, więc wpis nigdy nie stanie się kodem SQL.
Zapis wykonuje się tylko dla niepustego tekstu (if ($event !== '')). Sprawdzone na bazie: „zakupy” trafia do rekordu z 9 sierpnia - to sprawdza egzaminator w phpMyAdmin (R.5.7); „Urodziny Ani's” z apostrofem też zapisuje się poprawnie, a same spacje niczego nie zmieniają.
Typowe błędy
Tabela zadania z pliku baza5.sql ma kodowanie latin1, w którym nie ma liter „ś”, „ł”, „ą”: wpis „Śniadanie w Łodzi” zapisuje się jako „?niadanie w ?odzi” (sprawdzone). Zadanie tego nie ocenia; zmianę kodowania tabeli pokazuje wersja podstawowa.
organizer.phpPHPzmienione: 6 linii
<?phptry {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);// skrypt obsługujący formularz$event = trim($_POST['event'] ?? '');if ($event !== '') {// zmodyfikowane zapytanie 4 - wpis z formularza zawsze do dnia 2020-08-09$statement = $pdo->prepare('UPDATE zadania SET wpis = :entry WHERE dataZadania = :day');$statement->execute(['entry' => $event, 'day' => '2020-08-09']);$statement = null; }$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Dodatek spoza arkusza - klucz go nie ocenia (R.5.7 sprawdza tylko bazę). Strona, która odpowiada na formularz POST bezpośrednio, ma wadę: odświeżenie (F5) każe przeglądarce wysłać formularz jeszcze raz - pojawia się pytanie o ponowne przesłanie danych, a zapis wykonuje się drugi raz.
Wzorzec PRG (Post/Redirect/Get): po zapisie skrypt zamyka połączenie i odsyła kod 303 z nagłówkiem Location: organizer.php - przeglądarka sama otwiera stronę zwykłym żądaniem GET i dopiero ono rysuje kalendarz. exit jest konieczne: header() tylko ustawia nagłówek, a reszta pliku wykonywałaby się dalej. Odświeżenie powtarza już tylko GET.
header() działa, bo blok PHP stoi przed <!DOCTYPE html> - przed nim nie poszedł do przeglądarki żaden znak. Sprawdzone na prawdziwym PHP: odpowiedź na formularz to 303 na organizer.php, a baza ma wpis „zakupy”.
Typowe błędy
header() po wypisaniu choćby spacji przed <?php kończy się ostrzeżeniem „headers already sent” i przekierowanie nie działa.
organizer.phpPHPzmienione: 3 linie
<?phptry {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);// skrypt obsługujący formularz$event = trim($_POST['event'] ?? '');if ($event !== '') {// zmodyfikowane zapytanie 4 - wpis z formularza zawsze do dnia 2020-08-09$statement = $pdo->prepare('UPDATE zadania SET wpis = :entry WHERE dataZadania = :day');$statement->execute(['entry' => $event, 'day' => '2020-08-09']);$statement = null;$pdo = null;header('Location: organizer.php', true, 303); // PRG: po zapisie zwykłe wejście na stronęexit; }$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
R.5.3 Skrypt 1 wysyła do bazy danych zapytanie 1 lub skrypt obsługujący formularz wysyła zmodyfikowane zapytanie 4
ok. 2 min
Polecenie z arkusza
Działanie skryptu 1: Wysyła do bazy danych zapytanie 1
Zapytanie bez danych od użytkownika, więc wystarczy query; fetchAll() zwraca listę 31 dni w kolejności dat. Stoi po zapisie z formularza i przed $pdo = null, więc odczytuje już nowy wpis, a zamknięcie zostaje ostatnią operacją. W catch lista dostaje wartość pustą (R.5.3). Kalendarza jeszcze nie widać.
organizer.phpPHPzmienione: 4 linie
<?phptry {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);// skrypt obsługujący formularz$event = trim($_POST['event'] ?? '');if ($event !== '') {// zmodyfikowane zapytanie 4 - wpis z formularza zawsze do dnia 2020-08-09$statement = $pdo->prepare('UPDATE zadania SET wpis = :entry WHERE dataZadania = :day');$statement->execute(['entry' => $event, 'day' => '2020-08-09']);$statement = null;$pdo = null;header('Location: organizer.php', true, 303); // PRG: po zapisie zwykłe wejście na stronęexit; }// Skrypt 1: zapytanie 1 - dni sierpnia$days = $pdo->query("SELECT dataZadania, wpis FROM zadania WHERE miesiac = 'sierpien' ORDER BY dataZadania")->fetchAll();$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$days = [];$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Krok 28: Skrypt 1: blok kalendarza z datą dla każdego wiersza
Kryteria:
R.5.4 Skrypt 1 wyświetla w bloku głównym bloki zdefiniowane za pomocą znaczników sekcji. Liczba wygenerowanych bloków odpowiada liczbie wierszy zwróconych zapytaniem
ok. 3 min
Polecenie z arkusza
Za pomocą znacznika sekcji definiuje blok, a w nim wyświetla dane przesłane skryptem dotyczące jednego wiersza. Blok jest zgodny z Obrazem 3 oraz jest w nim wyświetlone: Data zadania w nagłówku piątego stopnia. Liczba wygenerowanych bloków odpowiada liczbie wierszy zwróconych zapytaniem
Pętla foreach po liście dni: jeden obieg to jeden <section class="day"> (R.5.4) - bloków jest tyle, ile wierszy zwróciło zapytanie, czyli 31. Data stoi w <h5>, a w nim w znaczniku <time datetime="..."> - na stronie wygląda tak samo, a przeglądarka i wyszukiwarka wiedzą, że to data. Funkcja e() przepuszcza tekst z bazy przez htmlspecialchars.
organizer.phpPHPzmienione: 10 linii
<?phpfunctione(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES);}try {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);// skrypt obsługujący formularz$event = trim($_POST['event'] ?? '');if ($event !== '') {// zmodyfikowane zapytanie 4 - wpis z formularza zawsze do dnia 2020-08-09$statement = $pdo->prepare('UPDATE zadania SET wpis = :entry WHERE dataZadania = :day');$statement->execute(['entry' => $event, 'day' => '2020-08-09']);$statement = null;$pdo = null;header('Location: organizer.php', true, 303); // PRG: po zapisie zwykłe wejście na stronęexit; }// Skrypt 1: zapytanie 1 - dni sierpnia$days = $pdo->query("SELECT dataZadania, wpis FROM zadania WHERE miesiac = 'sierpien' ORDER BY dataZadania")->fetchAll();$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$days = [];$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main><?phpforeach ($daysas$day): ?> <sectionclass="day"> <h5><timedatetime="<?= e($day['dataZadania']) ?>"><?= e($day['dataZadania']) ?></time></h5> </section><?phpendforeach; ?> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (organizer.php)
Krok 29: Skrypt 1: wpis w akapicie
Kryteria:
R.5.5 Skrypt 1 wyświetla datę zadania w nagłówku h5 oraz pole wpis w paragrafie
ok. 2 min
Polecenie z arkusza
Pole wpis w paragrafie (akapicie)
Wpis w <p> pod datą - blok wygląda jak na obrazie 3 (R.5.5). Dni bez zadań mają w bazie pusty tekst, więc ich akapit jest pusty.
Wpis pochodzi z formularza, więc e() jest tu konieczne: sprawdzone na prawdziwym PHP - wpis <b>zakupy</b> pokazuje się dosłownie, razem ze znakami, zamiast wykonać się jako znacznik (tak samo zostałby zatrzymany <script>). Podgląd: po wysłaniu „zakupy” i przekierowaniu blok 9 sierpnia ma nowy wpis - jak na obrazie 2.
organizer.phpPHPzmienione: 1 linia
<?phpfunctione(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES);}try {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);// skrypt obsługujący formularz$event = trim($_POST['event'] ?? '');if ($event !== '') {// zmodyfikowane zapytanie 4 - wpis z formularza zawsze do dnia 2020-08-09$statement = $pdo->prepare('UPDATE zadania SET wpis = :entry WHERE dataZadania = :day');$statement->execute(['entry' => $event, 'day' => '2020-08-09']);$statement = null;$pdo = null;header('Location: organizer.php', true, 303); // PRG: po zapisie zwykłe wejście na stronęexit; }// Skrypt 1: zapytanie 1 - dni sierpnia$days = $pdo->query("SELECT dataZadania, wpis FROM zadania WHERE miesiac = 'sierpien' ORDER BY dataZadania")->fetchAll();$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$days = [];$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main><?phpforeach ($daysas$day): ?> <sectionclass="day"> <h5><timedatetime="<?= e($day['dataZadania']) ?>"><?= e($day['dataZadania']) ?></time></h5> <p><?= e($day['wpis']) ?></p> </section><?phpendforeach; ?> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Podgląd strony po tym kroku (organizer.php)
Krok 30: Awaria bazy: komunikat
ok. 2 min
Dodatek spoza arkusza - klucz go nie ocenia. Gdy baza nie odpowiada, w bloku głównym pojawia się komunikat z role="alert" (czytnik ekranu przeczyta go od razu), a baner z formularzem i stopka działają dalej, bo lista dni ma w catch wartość pustą. Sprawdzone na prawdziwym PHP ze złym hasłem do bazy - bez ostrzeżeń PHP. Podgląd: zwykły kalendarz.
organizer.phpPHPzmienione: 3 linie
<?phpfunctione(string|int|float|null$value): string{returnhtmlspecialchars((string) $value, ENT_QUOTES);}try {$pdo = new PDO('mysql:host=localhost;dbname=kalendarz;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);// skrypt obsługujący formularz$event = trim($_POST['event'] ?? '');if ($event !== '') {// zmodyfikowane zapytanie 4 - wpis z formularza zawsze do dnia 2020-08-09$statement = $pdo->prepare('UPDATE zadania SET wpis = :entry WHERE dataZadania = :day');$statement->execute(['entry' => $event, 'day' => '2020-08-09']);$statement = null;$pdo = null;header('Location: organizer.php', true, 303); // PRG: po zapisie zwykłe wejście na stronęexit; }// Skrypt 1: zapytanie 1 - dni sierpnia$days = $pdo->query("SELECT dataZadania, wpis FROM zadania WHERE miesiac = 'sierpien' ORDER BY dataZadania")->fetchAll();$pdo = null; // zamknięcie połączenia - ostatnia operacja na bazie} catch (PDOException $exception) {$days = [];$databaseError = 'Nie udało się połączyć z kalendarzem. Spróbuj odświeżyć stronę za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <title>Sierpniowy kalendarz</title> <linkrel="stylesheet"href="styl5.css"></head><body> <header> <sectionid="title"> <h1>Organizer: SIERPIEŃ</h1> </section> <sectionid="add-event"> <formmethod="post"> <labelfor="event">Zapisz wydarzenie: </label> <inputtype="text"id="event"name="event"required> <buttontype="submit">OK</button> </form> </section> <sectionid="logo"> <imgsrc="logo2.png"alt="sierpień"> </section> </header> <main><?phpif (isset($databaseError)): ?> <prole="alert"><?= e($databaseError) ?></p><?phpendif; ?><?phpforeach ($daysas$day): ?> <sectionclass="day"> <h5><timedatetime="<?= e($day['dataZadania']) ?>"><?= e($day['dataZadania']) ?></time></h5> <p><?= e($day['wpis']) ?></p> </section><?phpendforeach; ?> </main> <footer> <p>Stronę wykonał: Bartosz Bryniarski</p> </footer></body></html>
Krok 31 · po tym etapie masz 34 z 34 kryteriów (pkt)
Krok 31: Co jest inaczej i którą wersję wybrać na egzaminie
ok. 3 min
Obie wersje dają komplet 34 punktów. Różnice:
wersja podstawowa
wersja nowoczesna
baner
trzy <header> z float i klasą banner
jeden <header> z siatką 40% / 40% / 20%
bloki kalendarza
float: left, stopka z clear
<main> z flex-wrap: wrap
kolory
wpisane w reguły
zmienne CSS
biblioteka
mysqli (tabela 1 arkusza)
PDO z wyjątkami
gdzie są skrypty
formularz w banerze, Skrypt 1 w bloku głównym
jeden blok na górze organizer.php
wpis w zapytaniu 4
wklejony do napisu SQL
parametr :entry
wpis z apostrofem
błąd i urwana strona
zapisuje się
po zapisie
odpowiedź na POST, F5 wysyła ponownie
przekierowanie 303 (PRG)
wypisywanie wpisu
wprost z bazy
e() i <time>
Oba warianty mają tę samą granicę: tabela zadania jest w latin1, więc polskie litery z formularza giną - poprawkę (zmianę kodowania tabeli) pokazuje wersja podstawowa, poza zadaniem egzaminacyjnym.
Dane logowania do bazy w prawdziwym projekcie trzyma się zwykle w osobnym pliku (np. config.php), ale arkusz każe napisać skrypt w pliku organizer.php - tu cały kod PHP zostaje w nim.
Na egzaminie wersja podstawowa trzyma się funkcji z tabeli 1 arkusza i łatwiej ją napisać bez dokumentacji. Nowoczesna zapisuje każdy tekst bezpiecznie i nie powtarza zapisu po odświeżeniu. Obie biblioteki klucz przyjmuje.
Krok 32 · po tym etapie masz 34 z 34 kryteriów (pkt)
Krok 32: Plik przeglądarka.txt i nagranie płyty
ok. 5 min
Polecenie z arkusza
Po zakończeniu pracy utwórz plik tekstowy. Zapisz w nim nazwę przeglądarki internetowej, w której weryfikowana była poprawność działania witryny. Nazwij plik przeglądarka.txt i zapisz go w folderze z numerem zdającego. Nagraj płytę z rezultatami pracy. W folderze powinny się znajdować następujące pliki: import.png, kw1.jpg, kw2.jpg, kw3.jpg, kw4.jpg, kwerendy.txt, logo2.png, organizer.php, przeglądarka.txt, styl5.css, ewentualnie inne przygotowane pliki.
W przeglądarka.txt nazwa przeglądarki, w której sprawdzana była strona przez http://localhost/.... Nagraj płytę, sprawdź jej odczyt i opisz ją numerem zdającego.
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.