INF.03-10-26.01-SGStyczeń 2026 · 48 kroków · ok. 90 min · 38 kryteriów z klucza
Rozwiązanie krok po kroku: Blog kulinarny z bazą danych (Nowoczesny kod (grid, PDO))
To drugi sposób rozwiązania tego samego arkusza: blog kulinarny z bazą przepisy (relacje n:1 i n:m), separatorem wyciętym z grafiki i stroną przepisy.php, na której wszystko - rodzaj, nazwa, trudność, alergeny, przepis i zdjęcie w tle sekcji - zależy od identyfikatora potrawy z adresu. Obie wersje spełniają wszystkie 38 kryteriów klucza - różni je sposób pisania kodu, nie wynik.
Czym ta wersja różni się od podstawowej:
siatka CSS10% 45% 45% zamiast flex, kolory w zmiennych CSS, tło nagłówka zapisem rgb(211 211 211 / 0.3),
odnośniki potraw generowane pętlą z tablicy PHP w bloku <nav>, każdy w osobnej linii dzięki display: block zamiast <br>,
zapytania z aliasami tabel,
PHP na PDO: cztery zapytania na górze pliku, wszystkie przygotowane i wysyłane przez jedną funkcję fetchForDish(); połączenie zamknięte przed HTML-em,
identyfikator z adresu sprawdzony filter_var z wartością domyślną 7, trudność z tablicy zamiast drabinki if, nazwa pliku w stylu inline zakodowana rawurlencode, komunikat dla potrawy spoza bazy, wartości z bazy przez escape().
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 - stan początkowy, czyli jagnięcina, jak na ilustracji 2. Baza i zdjęcia pochodzą z prawdziwego archiwum pliki14 (załącznik do arkusza, hasło w treści zadania).
Jak liczą się punkty. Klucz ma 38 kryteriów (R.1.1 - R.5.10), każde za 1 punkt. Próg zdania części praktycznej to 75%, czyli 29 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 38 kryteriów (pkt)
Krok 1: Baza danych przepisy
ok. 2 min
Polecenie z arkusza
Utwórz bazę danych o nazwie przepisy, z zestawem polskich znaków (np. utf8_polish_ci)
Archiwum pliki14.7z ma hasło BLog+#KuLinarnY@ (wielkie i małe litery na przemian, znaki +, #, @); w środku przepisy.sql, lista.txt, separator.png i zdjęcia potraw. W phpMyAdmin: Nowa, nazwa przepisy, porównywanie utf8_polish_ci, albo w zakładce SQL:
CREATE DATABASE przepisy CHARACTER SET utf8 COLLATE utf8_polish_ci;
Typowe błędy
Skrypty łączą się z bazą po nazwie - literówka w przepisy zabiera także punkt za połączenie (R.5.1).
Krok 2: Import tabel z pliku przepisy.sql
ok. 2 min
Polecenie z arkusza
Z rozpakowanego archiwum zaimportuj tabele z pliku przepisy.sql do utworzonej bazy
Zaznacz bazę przepisy, potem Import → przepisy.sql → Wykonaj. Cztery tabele z ilustracji 1: potrawy (klucz obcy idRodzaje - relacja n:1 z rodzaje), alergeny i tabela pośrednicząca lista_alergenow, która łączy potrawy z alergenami (relacja n:m).
Krok 3: Zrzut ekranu import.png
Kryteria:
R.1.1 Wykonany import tabel do bazy danych przepisy - obraz 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. Zrzuty zapytań są w formacie JPEG - tylko import idzie do PNG.
Typowe błędy
Wykadrowany zrzut albo zrzut bez paska zadań nie jest oceniany (uwaga w kluczu przy R.1).
Krok 4: Zapytanie 1: nazwa, trudność i kalorie potrawy 7
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 nazwę, trudność i kalorie potrawy o identyfikatorze równym 7. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nazwa, trudnosc, kalorie FROM potrawy WHERE idPotrawy = 7; oraz na zrzucie widoczne są dane: Jagnięcina, 3, 225
ok. 1 min
Polecenie z arkusza
Zapytanie 1: wybierające jedynie nazwę, trudność i kalorie potrawy o identyfikatorze równym 7
Trzy pola z tabeli potrawy, warunek na kluczu głównym idPotrawy. Wynik: Jagnięcina, trudność 3, 225 kalorii. Każda klauzula w osobnej linii - SQL nie zwraca uwagi na znaki nowej linii. Zapisz w kwerendy.txt, zrzut jako kw1.jpg.
R.1.4 Wykonane zapytanie 2 wybierające jedynie nazwę potrawy i odpowiadające jej pole rodzaj z tabeli rodzaje dla potrawy o identyfikatorze równym 7. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nazwa, rodzaj FROM potrawy JOIN rodzaje USING(idRodzaje) WHERE idPotrawy = 7; (lub porównanie kluczy po ON lub WHERE ) oraz na zrzucie widoczne są dane: Jagnięcina, dania mięsne
ok. 2 min
Polecenie z arkusza
Zapytanie 2: wybierające jedynie nazwę potrawy i odpowiadające jej pole rodzaj z tabeli rodzaje dla potrawy o identyfikatorze równym 7. Należy posłużyć się relacją
FROM potrawy AS p JOIN rodzaje AS r - aliasy tabel,
ON r.idRodzaje = p.idRodzaje - relacja n:1: wiele potraw tego samego rodzaju (klucz uznaje też krótsze USING(idRodzaje), bo kolumna ma w obu tabelach tę samą nazwę),
WHERE p.idPotrawy = 7,
SELECT p.nazwa, r.rodzaj.
Wynik: Jagnięcina, dania mięsne. Zrzut: kw2.jpg.
kwerendy.txtTekstzmienione: 6 linii
-- Zapytanie 1SELECT nazwa, trudnosc, kalorieFROM potrawyWHERE idPotrawy = 7;-- Zapytanie 2SELECT p.nazwa, r.rodzajFROM potrawy AS pJOIN rodzaje AS r ON r.idRodzaje = p.idRodzajeWHERE p.idPotrawy = 7;
Podgląd strony po tym kroku
Krok 6: Zapytanie 3: alergeny potrawy 7 przez relację n:m
Kryteria:
R.1.5 Wykonane zapytanie 3 wybierające jedynie nazwę potrawy i odpowiadające jej nazwy alergenów dla potrawy o identyfikatorze równym 7. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nazwa, alergen FROM potrawy JOIN lista_alergenow USING(idPotrawy) JOIN alergeny USING(idAlergeny) WHERE idPotrawy=7; (lub porównanie kluczy po ON lub WHERE ) oraz na zrzucie widoczne są dane: Jagnięcina, seler
ok. 3 min
Polecenie z arkusza
Zapytanie 3: wybierające jedynie nazwę potrawy i odpowiadające jej nazwy alergenów dla potrawy o identyfikatorze równym 7. Zapytanie wykorzystuje relację n:m pomiędzy trzema tabelami
FROM potrawy AS p JOIN lista_alergenow AS la ON la.idPotrawy = p.idPotrawy - od potrawy do tabeli pośredniczącej,
JOIN alergeny AS a ON a.idAlergeny = la.idAlergeny - od tabeli pośredniczącej do alergenu,
WHERE p.idPotrawy = 7.
Przy trzech tabelach aliasy skracają zapytanie najbardziej. Jagnięcina ma jeden alergen: seler; kociołek - trzy, i wtedy wynik ma trzy wiersze. Zrzut: kw3.jpg.
kwerendy.txtTekstzmienione: 7 linii
-- Zapytanie 1SELECT nazwa, trudnosc, kalorieFROM potrawyWHERE idPotrawy = 7;-- Zapytanie 2SELECT p.nazwa, r.rodzajFROM potrawy AS pJOIN rodzaje AS r ON r.idRodzaje = p.idRodzajeWHERE p.idPotrawy = 7;-- Zapytanie 3SELECT p.nazwa, a.alergenFROM potrawy AS pJOIN lista_alergenow AS la ON la.idPotrawy = p.idPotrawyJOIN alergeny AS a ON a.idAlergeny = la.idAlergenyWHERE p.idPotrawy = 7;
Podgląd strony po tym kroku
Krok 7: Zapytanie 4: przepis i plik zdjęcia
Kryteria:
R.1.6 Wykonane zapytanie 4 wybierające jedynie pola przepis i plik dla potrawy o identyfikatorze równym 7. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT przepis, plik FROM potrawy WHERE idPotrawy = 7; oraz na zrzucie widoczne są dane: "Mięso oczyść ...." i jagniecina.jpg
ok. 1 min
Polecenie z arkusza
Zapytanie 4: wybierające jedynie pola przepis i plik dla potrawy o identyfikatorze równym 7
Dwa pola: długi tekst przepisu („Mięso oczyść z błon…”) i nazwa pliku jagniecina.jpg. phpMyAdmin skraca długi tekst w podglądzie. Zrzut: kw4.jpg.
kwerendy.txtTekstzmienione: 5 linii
-- Zapytanie 1SELECT nazwa, trudnosc, kalorieFROM potrawyWHERE idPotrawy = 7;-- Zapytanie 2SELECT p.nazwa, r.rodzajFROM potrawy AS pJOIN rodzaje AS r ON r.idRodzaje = p.idRodzajeWHERE p.idPotrawy = 7;-- Zapytanie 3SELECT p.nazwa, a.alergenFROM potrawy AS pJOIN lista_alergenow AS la ON la.idPotrawy = p.idPotrawyJOIN alergeny AS a ON a.idAlergeny = la.idAlergenyWHERE p.idPotrawy = 7;-- Zapytanie 4SELECT przepis, plikFROM potrawyWHERE idPotrawy = 7;
Podgląd strony po tym kroku
Etap 2: Grafika i strona HTML
Kroki 8-25 · po tym etapie masz 17 z 38 kryteriów (pkt)
Krok 8: Grafika separator.png: kadr do pierwszego separatora
Kryteria:
R.2.1 Grafika separator.png jest skadrowana tak, że jest widoczny tylko separator zgodny z ilustracją 3. Grafika jest przycięta do krawędzi separatora (sprawdzić bez powiększania)
ok. 3 min
Polecenie z arkusza
Cechy grafiki separator.png: Grafikę należy skadrować tak, aby był widoczny jedynie separator pierwszy od góry, po lewej stronie. Grafikę należy dociąć do krawędzi separatora (ilustracja 3, ramka przedstawia kadr obrazu).
Grafika jest taka sama w obu wersjach. W GIMP-ie zaznacz pierwszy separator od góry po lewej z zapasem (bez sąsiednich ozdobników), Obraz → Przytnij do zaznaczenia, potem Obraz → Przytnij do zawartości - krawędzie dosuną się do linii i ornamentu. Wynik: 317 × 57 px, eksport do PNG z przezroczystością (R.2.1).
Podgląd strony po tym kroku
Krok 9: Plik przepisy.php w HTML5
ok. 2 min
Polecenie z arkusza
Składa się ze strony o nazwie przepisy.php. Zapisana w języku HTML5
<!DOCTYPE html> i szkielet. Rozszerzenie .php, bo dojdą skrypty - strona otwiera się przez http://localhost/....
R.3.2 Strona zawiera działające połączenie z zewnętrznym arkuszem stylów o nazwie styl.css formatowanie CSS pochodzi jedynie z tego arkusza (oprócz stylu inline sekcji)
ok. 1 min
Polecenie z arkusza
Arkusz stylów w pliku o nazwie styl.css prawidłowo połączony z kodem strony
Plik styl.css. Jedynym wyjątkiem od „cały CSS w pliku” będzie styl inline tła sekcji, którego arkusz wprost wymaga.
R.2.4 W sekcji body zdefiniowany jest podział na bloki za pomocą semantycznych znaczników HTML5: aside+main+section (dopuszcza się zastosowanie bloków pomocniczych)
ok. 1 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ą 4
Trzy bloki z ilustracji 4: aside, main i section (R.2.4). Kolejność w kodzie jest kolejnością kolumn siatki, którą ustawi CSS - identyfikatory nie są potrzebne.
Krok 15: Odnośnik „Sernik” z identyfikatorem 1 - z tablicy
ok. 3 min
Polecenie z arkusza
Zawartość bloku bocznego: Każdy odnośnik prowadzi do pliku przepisy.php oraz przesyła do niego metodą GET wartość id odpowiadającą wybranej potrawie, są to 1 dla odnośnika Sernik, 2 dla Sałatka itd.
Dziesięć odnośników różni się tylko numerem i nazwą, więc na górze pliku powstaje tablica$dishes (klucz = identyfikator potrawy, wartość = nazwa z lista.txt), a w bloku <nav> pętla foreach w składni alternatywnej wypisuje <a href="przepisy.php?id=1">Sernik</a> (tabela 3 arkusza). <?= ... ?> to skrót od <?php echo ... ?>.
Nazw nie bierze się z bazy, bo baza ma inne: „Sernik z brzoskwiniami”, „Prosta sałatka” - a klucz sprawdza krótkie nazwy z lista.txt. Od tego kroku plik zawiera PHP, więc podgląd to wynik jego uruchomienia.
R.2.6 W bloku bocznym znajduje się 10 odnośników o treści: Sernik, Sałatka, Pankejki, Nugetsy, Łosoś, Kociołek, Jagnięcina, Hamburgery, Eklerki, Churros
R.3.4 Kliknięcie dowolnego odnośnika odświeża stronę przepisy.php oraz przesyła wartość id potrawy, której odnośnik kliknięto, URL jest postaci, np.: przepisy.php?id=1 (sprawdzić dla przynajmniej trzech odnośników)
ok. 3 min
Polecenie z arkusza
10 odnośników z nazwami potraw (można skopiować z pliku lista.txt): Sernik, Sałatka ... Każdy odnośnik umieszczony jest w osobnej linii
Tablica ma teraz dziesięć par skopiowanych z lista.txt, każda w osobnej linii (przecinek po ostatniej jest dozwolony). Numer i nazwa stoją w jednej parze, więc nie mogą się rozjechać jak przy kopiowaniu linii HTML (R.2.6, R.3.4).
„Każdy odnośnik w osobnej linii” zrobi CSS (display: block w regule a) zamiast <br> po każdym odnośniku - na razie odnośniki stoją obok siebie.
R.3.3 Do bloku sekcji przypisane jest formatowanie inline tła w postaci obrazu
ok. 2 min
Polecenie z arkusza
Zawartość bloku sekcji: Przypisany jak atrybut znacznika bloku styl CSS inline: tło wypełnione obrazem, którego nazwa pliku została pobrana skryptem 4
Styl inline to atrybut style w samym znaczniku - arkusz wprost go wymaga (R.3.3), bo zdjęcie zależy od potrawy, a plik CSS jest stały. Na razie nazwa pliku wpisana na stałe; skrypt 4 podmieni ją na nazwę z bazy.
R.2.5 W bloku bocznym znajduje się <p> z numerem zdającego, w bloku głównym <h1> i <p> (w HTML lub skrypcie ) oraz <h2> "Składniki", w bloku sekcji <h1>: "Blog Kulinarny"
ok. 1 min
Polecenie z arkusza
Nagłówek pierwszego stopnia o treści „Blog Kulinarny”
Drugi <h1> na stronie, w sekcji. Komplet nagłówków i akapitów z klucza (R.2.5).
Kroki 26-35 · po tym etapie masz 28 z 38 kryteriów (pkt)
Krok 26: Plik styl.css: zmienne kolorów, krój i wyrównanie
Kryteria:
R.4.1 domyślne formatowanie selektorów (*): krój czcionki Garamond, wyrównanie tekstu do środka
ok. 2 min
Polecenie z arkusza
Domyślnie dla wszystkich selektorów: krój czcionki Garamond, wyrównanie tekstu do środka
Kolory z arkusza w zmiennych CSS w :root. Tło nagłówka zapisane nowszą składnią rgb(211 211 211 / 0.3): składowe po spacjach, krycie po ukośniku - to ten sam kolor co rgba(211, 211, 211, 0.3) i tak też pokazuje go inspektor.
Selektor *: krój Garamond z rodziną zapasową serif i środkowanie (R.4.1).
R.4.5 przepełnienie bloku: blok główny auto styl bloku sekcji: background-size: cover;
ok. 2 min
Polecenie z arkusza
Dla bloku sekcji: wymiary obrazu tła są automatycznie dostosowane do wymiaru bloku przez obcięcie lub rozciągnięcie obrazu, biały kolor czcionki, szerokość 45%, wysokość 600 px
background-size: cover (tabela 2 arkusza) skaluje obraz, żeby przykrył cały blok, i obcina nadmiar. Obraz przychodzi ze stylu inline, dopasowanie - z pliku CSS; obie deklaracje działają razem (R.4.5).
R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z ilustracją 4 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)
R.4.4 wysokość: blok boczny, główny, sekcji - 600px szerokość: blok boczny - 10%, główny i sekcji - 45% (width lub grid-template-columns lub flex )
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ą 4
<body> staje się siatką o trzech kolumnach: grid-template-columns: 10% 45% 45%. aside, main i section trafiają po kolei do kolumn - stoją obok siebie, a szerokości z arkusza wynikają z kolumn (R.3.1). Klucz dopuszcza szerokości z grid-template-columns, więc razem z wysokościami z poprzednich reguł komplet wymiarów jest gotowy (R.4.4).
R.4.2 kolor tła: blok boczny - #3A3C3E, selektor h1 - RGBA(211, 211, 211, 0.3) (lub z opacity )
R.4.6 styl nagłówka pierwszego stopnia: marginesy wewnętrzne (padding ): 20px góra i dół marginesy zewnętrzne (margin ): 0
ok. 2 min
Polecenie z arkusza
Dla selektora nagłówka pierwszego stopnia: kolor tła RGB 211, 211, 211, przezroczystość 0.3, marginesy wewnętrzne górny i dolny 20 px pozostałe 0, marginesy zewnętrzne 0, rozmiar czcionki 2 em
Przezroczystość w samym kolorze tła, nie opacity - opacity przezroczyściłaby też napis. padding: 20px 0 - góra i dół 20 px, boki 0. Selektor znacznika h1 (uwaga arkusza) obejmuje oba nagłówki (R.4.2, R.4.6).
Kolor, rozmiar i kursywa z arkusza (R.4.3, R.4.7, R.4.8), a do tego display: block: odnośnik zajmuje całą szerokość bloku bocznego, więc każdy stoi w osobnej linii - zamiast <br> po każdym. Przy okazji kliknąć można w całą szerokość wiersza, nie tylko w napis.
Kroki 36-45 · po tym etapie masz 38 z 38 kryteriów (pkt)
Krok 36: Połączenie PDO na górze pliku
Kryteria:
R.5.1 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy przepisy oraz ostatnią operacją na bazie jest jej zamknięcie
ok. 4 min
Polecenie z arkusza
Napisane w języku PHP. Należy stosować znaczące nazewnictwo zmiennych w języku polskim lub angielskim. Łączą się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie przepisy. Na końcu skrypt zamyka połączenie z bazą
Praca z bazą stoi przed<!DOCTYPE html>, obok tablicy potraw: najpierw dane, potem HTML, który je tylko wypisuje. Połączenie przez PDO (klucz R.5 przyjmuje MySQLi i PDO):
DSN 'mysql:host=localhost;dbname=przepisy;charset=utf8mb4' - serwer, baza i kodowanie; 'root', '' - użytkownik i puste hasło,
ERRMODE_EXCEPTION - błąd bazy rzuca wyjątek, FETCH_ASSOC - wiersze jako tablice z nazwami kolumn,
$pdo = null zamyka połączenie - cztery zapytania staną przed tą linią, więc zamknięcie zostanie ostatnią operacją na bazie (R.5.1),
try / catch - gdy baza nie działa, szczegóły idą do dziennika serwera, a w bloku głównym stoi ogólny komunikat $dbError,
funkcja escape() to krótka nazwa htmlspecialchars(..., ENT_QUOTES) dla wartości z bazy.
Krok 37: Identyfikator potrawy: 7 albo wartość z adresu
ok. 3 min
Polecenie z arkusza
Jeśli do strony została przesłana wartość metodą GET, zmienna jest inicjowana tą wartością. W przeciwnym wypadku, zmienna jest inicjowana wartością 7. Dalej w opisie skryptów zmienna ta jest nazywana ID
Zmienna $dishId (ID z arkusza) startuje z wartością 7 - jagnięcina przy pierwszym wejściu. Gdy w adresie jest ?id=... (isset), wartość przechodzi przez filter_var z FILTER_VALIDATE_INT: liczba całkowita od 1 trafia do zmiennej, a cokolwiek innego (abc, 7 OR 1=1, -3) zamienia się na default = 7. Zmienna zawsze jest więc liczbą - nic innego nie dotrze do zapytań.
Krok 38: Skrypt 1: zapytanie 2 przez funkcję fetchForDish()
Kryteria:
R.5.3 Przynajmniej jeden skrypt wysyła zapytanie, zmodyfikowane w ten sposób że sprawdzana jest wartość ze zmiennej zdefiniowanej w R.5.2
ok. 3 min
Polecenie z arkusza
Skrypt 1: Wysyła do bazy danych zapytanie 2, zmodyfikowane tak, że sprawdzana jest zmienna ID
Wszystkie cztery skrypty wysyłają zapytanie o tę samą potrawę, więc wysyłanie zapisane jest raz - w funkcji fetchForDish($pdo, $sql, $dishId): prepare wysyła zapytanie z parametrem :id, execute(['id' => $dishId]) - wartość osobno, fetchAll() oddaje wszystkie wiersze.
Skrypt 1: zapytanie 2 z aliasami, w którym zamiast 7 stoi :id (R.5.3). Wynik ma najwyżej jeden wiersz, więc [0] ?? null bierze pierwszy albo null, gdy potrawy nie ma.
R.5.4 Na stronie wyświetlany jest rodzaj potrawy zwrócony z bazy danych
ok. 1 min
Polecenie z arkusza
Wyświetla wartość zwróconą polem rodzaj
W <h1> bloku głównego pole rodzaj przez escape(); ?? '' daje pusty napis, gdy potrawy nie ma. Dla jagnięciny: „dania mięsne” na półprzezroczystym pasku (R.5.4).
Skrypt 2: Wysyła do bazy danych zapytanie 1, zmodyfikowane tak, że sprawdzana jest zmienna ID. Wyświetla: Nagłówek drugiego stopnia z nazwą potrawy
Ta sama funkcja z zapytaniem 1 - jedna linia zamiast czterech. W HTML-u, między nagłówkiem a separatorem, warunek: gdy potrawa się znalazła, jej nazwa w <h2> (kapitaliki z CSS).
R.5.5 Na stronie wyświetlana jest trudność na podstawie pola trudnosc z bazy danych: „łatwe” (trudnosc = 1) lub „średnie” (trudnosc = 2) lub „trudne” (trudnosc = 3)
R.5.6 Na stronie wyświetlana jest nazwa potrawy i wartość kalorii, które zostały zwrócone zapytaniem
ok. 3 min
Polecenie z arkusza
Paragraf o treści „Trudność: <trudnosc>, Kalorie: <kalorie>”, pola <> są ustalone na podstawie wartości z bazy danych, a <trudnosc> przyjmuje wartości: „łatwe” dla trudnosc=1, „średnie” dla trudnosc=2, „trudne” dla trudnosc=3
Zamiana liczby na słowo to tłumaczenie według tabelki, więc na górze pliku stoi tablica$levels = [1 => 'łatwe', 2 => 'średnie', 3 => 'trudne'], a w akapicie $levels[$dish['trudnosc']] (R.5.5). Gdyby w bazie pojawiła się trudność 4, ?? 'nieznana' zamiast ostrzeżenia wypisze słowo. Kalorie z bazy bez zmian (R.5.6). Sprawdzone: sałatka - łatwe, sernik - średnie, kociołek - trudne.
R.5.7 Na stronie wyświetlane są alergeny zwrócone zapytaniem, zastosowano pętlę dla wszystkich zwróconych rekordów
ok. 3 min
Polecenie z arkusza
Skrypt 3: Wysyła do bazy danych zapytanie 3, zmodyfikowane tak, że sprawdzana jest zmienna ID. Wyświetla oddzielone spacją nazwy wszystkich alergenów zwróconych z bazy
Zapytanie 3 przez tę samą funkcję. Pętla foreach przechodzi po wszystkich zwróconych wierszach i zbiera same nazwy do tablicy $allergenNames (klucz wymaga pętli dla wszystkich rekordów - R.5.7). W akapicie implode(' ', $allergenNames) skleja je spacją - tylko między nazwami, bez spacji na końcu.
Dla jagnięciny: „seler”; po kliknięciu „Kociołek” (?id=6): „gluten laktoza orzechy”.
R.5.8 Na stronie wyświetlana jest zawartość pola przepis
ok. 2 min
Polecenie z arkusza
Skrypt 4: Wysyła do bazy danych zapytanie 4, zmodyfikowane tak, że sprawdzana jest zmienna ID. Wyświetla wartość pola przepis zwróconą zapytaniem
Czwarte zapytanie przez funkcję, ostatnie przed $pdo = null - zamknięcie połączenia nadal jest ostatnią operacją na bazie. Pole przepis w akapicie na końcu bloku głównego (R.5.8). Przepis jagnięciny jest długi - w węższym oknie, jak na ilustracji 2, w bloku głównym pojawia się pasek przewijania.
przepisy.phpPHPzmienione: 4 linie
<?phpfunctionescape(?string $text): string{returnhtmlspecialchars($text ?? '', ENT_QUOTES);}functionfetchForDish(PDO $pdo, string $sql, int $dishId): array{$statement = $pdo->prepare($sql);$statement->execute(['id' => $dishId]);return$statement->fetchAll();}$dishes = [1 => 'Sernik',2 => 'Sałatka',3 => 'Pankejki',4 => 'Nugetsy',5 => 'Łosoś',6 => 'Kociołek',7 => 'Jagnięcina',8 => 'Hamburgery',9 => 'Eklerki',10 => 'Churros',];$levels = [1 => 'łatwe', 2 => 'średnie', 3 => 'trudne'];$dishId = 7;if (isset($_GET['id'])) {$dishId = filter_var($_GET['id'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'default' => 7]]);}$category = null;$dish = null;$allergenNames = [];$recipe = null;$dbError = '';try {$pdo = new PDO('mysql:host=localhost;dbname=przepisy;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);$category = fetchForDish($pdo,'SELECT p.nazwa, r.rodzaj FROM potrawy AS p JOIN rodzaje AS r ON r.idRodzaje = p.idRodzaje WHERE p.idPotrawy = :id', $dishId)[0] ?? null;$dish = fetchForDish($pdo,'SELECT nazwa, trudnosc, kalorie FROM potrawy WHERE idPotrawy = :id', $dishId)[0] ?? null;$allergenRows = fetchForDish($pdo,'SELECT p.nazwa, a.alergen FROM potrawy AS p JOIN lista_alergenow AS la ON la.idPotrawy = p.idPotrawy JOIN alergeny AS a ON a.idAlergeny = la.idAlergeny WHERE p.idPotrawy = :id', $dishId);foreach ($allergenRowsas$allergenRow) {$allergenNames[] = $allergenRow['alergen']; }$recipe = fetchForDish($pdo,'SELECT przepis, plik FROM potrawy WHERE idPotrawy = :id', $dishId)[0] ?? null;$pdo = null;} catch (PDOException $exception) {error_log($exception->getMessage());$dbError = 'Nie udało się pobrać przepisu. Spróbuj ponownie za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <title>Blog kulinarny</title> <linkrel="stylesheet"href="styl.css"></head><body> <aside> <navaria-label="Potrawy"><?phpforeach ($dishesas$id => $name): ?> <ahref="przepisy.php?id=<?= $id?>"><?= $name?></a><?phpendforeach; ?> </nav> <p>Autor: Bartosz Bryniarski</p> </aside> <main><?phpif ($dbError !== ''): ?> <p><?= $dbError?></p><?phpendif; ?> <h1><?= escape($category['rodzaj'] ?? '') ?></h1><?phpif ($dish !== null): ?> <h2><?= escape($dish['nazwa']) ?></h2> <p>Trudność: <?= $levels[$dish['trudnosc']] ?? 'nieznana'?>, Kalorie: <?= escape($dish['kalorie']) ?></p><?phpendif; ?> <imgsrc="separator.png"alt="przepis"> <p>Alergeny: <?= escape(implode(' ', $allergenNames)) ?></p> <h2>Składniki</h2> <ul> <li>Lorem 1 kg</li> <li>Ipsum 2 szt.</li> <li>Dolor 200 g</li> <li>Sit amet (szczypta)</li> </ul> <p><?= escape($recipe['przepis'] ?? '') ?></p> </main> <sectionstyle="background-image: url('jagniecina.jpg');"> <h1>Blog Kulinarny</h1> </section></body></html>
Podgląd strony po tym kroku (przepisy.php)
Krok 44: Tło sekcji z nazwy pliku z bazy
Kryteria: , ,
R.5.2 Zmienna identyfikatora potrawy jest inicjowana wartością 7, następnie wartością przesłaną metodą GET do strony. Wygenerowana zawartość strony (w tym tło sekcji) zawsze dotyczy jednej potrawy - zgodnej z klikniętym odnośnikiem, w stanie początkowym zawartość dotyczy jagnięciny (sprawdzić dla co najmniej trzech odnośników)
R.5.9 Tło sekcji jest wyświetlane z pliku o nazwie zwróconej zapytaniem w skrypcie 4
R.5.10 W skryptach zastosowana przynajmniej jedna zmienna oraz nazwy wszystkich zmiennych są w języku polskim lub angielskim i odzwierciedlają cel zastosowania
ok. 3 min
Polecenie z arkusza
Pole plik pobrane zapytaniem w tym skrypcie powinno zostać użyte do ustawienia tła sekcji stylem CSS inline, o czym jest mowa w części „Cechy witryny”, „Zawartość bloku sekcji”
W stylu inline zamiast stałej nazwy stoi pole plik z zapytania 4 (R.5.9). Nazwa pliku trafia do adresu w CSS, więc przechodzi przez dwie funkcje: rawurlencode zamienia spacje i znaki specjalne na zapis adresu (moje zdjęcie.jpg → moje%20zdj%C4%99cie.jpg), a escape() pilnuje cudzysłowów atrybutu. Gdy potrawy nie ma, sekcja nie dostaje stylu.
Teraz cała strona zależy od jednej zmiennej $dishId: przy pierwszym wejściu jagnięcina, po kliknięciu - potrawa z odnośnika, razem ze zdjęciem w tle (sprawdzone dla sernika, łososia i churros; R.5.2). Nazwy zmiennych i funkcji mówią, co przechowują i robią (R.5.10): $dishes, $levels, $dishId, $category, $dish, $allergenRows, $allergenNames, $recipe, $pdo, $dbError, escape(), fetchForDish().
Jeśli do strony została przesłana wartość metodą GET, zmienna jest inicjowana tą wartością. W przeciwnym wypadku, zmienna jest inicjowana wartością 7
Sprawdzone adresy: ?id=99 (liczba poprawna, ale takiej potrawy nie ma) - wszystkie zapytania zwracają zero wierszy, strona pokazuje „Nie ma potrawy o numerze 99.” zamiast pustych nagłówków i ostrzeżeń PHP, a sekcja nie ma tła. ?id=7 OR 1=1 - filter_var odrzuca tekst i zostaje jagnięcina; wersja z egzaminu pokazałaby w tym miejscu alergeny wszystkich potraw. Podgląd: przepisy.php?id=99. Klucz tego nie sprawdza.
przepisy.phpPHPzmienione: 2 linie
<?phpfunctionescape(?string $text): string{returnhtmlspecialchars($text ?? '', ENT_QUOTES);}functionfetchForDish(PDO $pdo, string $sql, int $dishId): array{$statement = $pdo->prepare($sql);$statement->execute(['id' => $dishId]);return$statement->fetchAll();}$dishes = [1 => 'Sernik',2 => 'Sałatka',3 => 'Pankejki',4 => 'Nugetsy',5 => 'Łosoś',6 => 'Kociołek',7 => 'Jagnięcina',8 => 'Hamburgery',9 => 'Eklerki',10 => 'Churros',];$levels = [1 => 'łatwe', 2 => 'średnie', 3 => 'trudne'];$dishId = 7;if (isset($_GET['id'])) {$dishId = filter_var($_GET['id'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'default' => 7]]);}$category = null;$dish = null;$allergenNames = [];$recipe = null;$dbError = '';try {$pdo = new PDO('mysql:host=localhost;dbname=przepisy;charset=utf8mb4', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);$category = fetchForDish($pdo,'SELECT p.nazwa, r.rodzaj FROM potrawy AS p JOIN rodzaje AS r ON r.idRodzaje = p.idRodzaje WHERE p.idPotrawy = :id', $dishId)[0] ?? null;$dish = fetchForDish($pdo,'SELECT nazwa, trudnosc, kalorie FROM potrawy WHERE idPotrawy = :id', $dishId)[0] ?? null;$allergenRows = fetchForDish($pdo,'SELECT p.nazwa, a.alergen FROM potrawy AS p JOIN lista_alergenow AS la ON la.idPotrawy = p.idPotrawy JOIN alergeny AS a ON a.idAlergeny = la.idAlergeny WHERE p.idPotrawy = :id', $dishId);foreach ($allergenRowsas$allergenRow) {$allergenNames[] = $allergenRow['alergen']; }$recipe = fetchForDish($pdo,'SELECT przepis, plik FROM potrawy WHERE idPotrawy = :id', $dishId)[0] ?? null;$pdo = null;} catch (PDOException $exception) {error_log($exception->getMessage());$dbError = 'Nie udało się pobrać przepisu. Spróbuj ponownie za chwilę.';}?><!DOCTYPE html><htmllang="pl"><head> <metacharset="UTF-8"> <title>Blog kulinarny</title> <linkrel="stylesheet"href="styl.css"></head><body> <aside> <navaria-label="Potrawy"><?phpforeach ($dishesas$id => $name): ?> <ahref="przepisy.php?id=<?= $id?>"><?= $name?></a><?phpendforeach; ?> </nav> <p>Autor: Bartosz Bryniarski</p> </aside> <main><?phpif ($dbError !== ''): ?> <p><?= $dbError?></p><?phpelseif ($dish === null): ?> <p>Nie ma potrawy o numerze <?= $dishId?>.</p><?phpendif; ?> <h1><?= escape($category['rodzaj'] ?? '') ?></h1><?phpif ($dish !== null): ?> <h2><?= escape($dish['nazwa']) ?></h2> <p>Trudność: <?= $levels[$dish['trudnosc']] ?? 'nieznana'?>, Kalorie: <?= escape($dish['kalorie']) ?></p><?phpendif; ?> <imgsrc="separator.png"alt="przepis"> <p>Alergeny: <?= escape(implode(' ', $allergenNames)) ?></p> <h2>Składniki</h2> <ul> <li>Lorem 1 kg</li> <li>Ipsum 2 szt.</li> <li>Dolor 200 g</li> <li>Sit amet (szczypta)</li> </ul> <p><?= escape($recipe['przepis'] ?? '') ?></p> </main><?phpif ($recipe !== null): ?> <sectionstyle="background-image: url('<?= escape(rawurlencode($recipe['plik'])) ?>');"><?phpelse: ?> <section><?phpendif; ?> <h1>Blog Kulinarny</h1> </section></body></html>
Podgląd strony po tym kroku (przepisy.php)
Etap 5: Porównanie z wersją podstawową
Kroki 46-47 · po tym etapie masz 38 z 38 kryteriów (pkt)
Krok 46: Co jest inaczej i dlaczego
ok. 3 min
Polecenie z arkusza
Ocenie podlegać będzie 5 rezultatów: operacje na bazie danych, zawartość witryny internetowej, działanie witryny internetowej, styl CSS witryny internetowej, skrypt połączenia z bazą.
Czas przeznaczony na wykonanie zadania wynosi 150 minut.
Wersja podstawowa, gdy liczy się czas albo PDO nie jest opanowane: cztery krótkie skrypty z funkcjami z tabeli 1 arkusza stoją tam, gdzie widać ich efekt.
Ta wersja, gdy funkcje i PDO są pisane z pamięci. Funkcja fetchForDish() opłaca się tu wyjątkowo: cztery zapytania różnią się tylko treścią SQL, więc błąd w przygotowaniu zapytania poprawia się w jednym miejscu.
Mieszanie też daje pełne punkty: siatka i tablica $levels z tej wersji, a mysqli_* z podstawowej.
Krok 48 · po tym etapie masz 38 z 38 kryteriów (pkt)
Krok 48: Plik przeglądarka.txt i nagranie płyty
ok. 5 min
Polecenie z arkusza
UWAGA: 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 przez http://localhost/…/przepisy.php.
W folderze z numerem zdającego: zdjęcia potraw z archiwum, import.png, kw1.jpg - kw4.jpg, kwerendy.txt, przeglądarka.txt, przepisy.php, separator.png, styl.css. Arkusz wymienia kociołek.jpg z „ł”, ale w archiwum i w bazie jest kociolek.jpg - zostaje nazwa z archiwum, bo tej szuka strona. 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.