Przejdź do głównej treści
INF.03-06-25.06-SGCzerwiec 2025 · 47 kroków · ok. 95 min · 42 kryteria z klucza

Rozwiązanie krok po kroku: Portal biblioteki internetowej (Nowoczesny kod (grid, PDO))

To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza biblioteka z pięcioma operacjami, przezroczysta grafika, strona biblioteka.php z czterema sekcjami i czterema skryptami - i tak samo spełnia każde kryterium klucza. Inna jest droga:

  • siatka CSS w podejściu „mobile first”: domyślnie sekcje stoją jedna pod drugą (wąski ekran), a jedno zapytanie @media (min-width: 801px) ustawia je w czterech kolumnach - bez float i clear,
  • PHP na PDO: jedno zapytanie przygotowane wykonane kolejno dla trzech gatunków i pętla po tablicy sekcji zamiast trzech skopiowanych skryptów, filter_input i biała lista dla id książki, przekierowanie po rezerwacji (PRG), funkcja e() przy każdym wypisaniu i dane logowania w config.php.

Arkusz każe skrypty 2 i 3 „powielić i dostosować”; klucz ocenia, czy działają we wszystkich trzech sekcjach (R.5.8), a nie liczbę kopii - pętla daje ten sam wynik bez powtórzeń. Wersja podstawowa (zakładka obok) robi to najpierw kopiami, a funkcje wprowadza na końcu. Na końcu jest krótkie porównanie obu wersji.

Grafika i baza pochodzą z prawdziwego archiwum pliki4 (załącznik na stronie arkusza), a podgląd każdego kroku z PHP to wynik prawdziwego uruchomienia strony na serwerze z tą bazą.

Jak liczą się punkty. Klucz ma 42 kryteria (R.1.1 - R.5.10), każde za 1 punkt. Próg zdania to 75%, czyli 32 punkty. Przy każdym kroku jest napisane, które kryteria spełnia.

  1. Etap 1: Baza danych

    Kroki 1-8 · po tym etapie masz 7 z 42 kryteriów (pkt)

    Krok 1: Baza danych biblioteka

    Kryteria:

    R.1.1 Wykonany import tabeli do bazy danych biblioteka , czynność udokumentowana w pliku import.png

    ok. 2 min
    Polecenie z arkusza

    Utwórz bazę danych o nazwie biblioteka, z zestawem polskich znaków (np. utf8_unicode_ci).

    phpMyAdmin → Nowa → nazwa biblioteka, porównywanie utf8_unicode_ci. W SQL:

    CREATE DATABASE biblioteka CHARACTER SET utf8 COLLATE utf8_unicode_ci;
    
    Typowe błędy

    Literówka w nazwie bazy odcina też punkty za skrypty - połączenie podaje nazwę bazy (R.5.1).

  2. Krok 2: Import tabel z pliku biblio.sql

    Kryteria:

    R.1.1 Wykonany import tabeli do bazy danych biblioteka , czynność udokumentowana w pliku import.png

    ok. 2 min
    Polecenie z arkusza

    Z rozpakowanego archiwum zaimportuj tabele z pliku biblio.sql do utworzonej bazy.

    Zaznacz bazę biblioteka, karta Import, plik biblio.sql, Wykonaj. Dwie tabele z ilustracji 1: ksiazka (20 książek, gatunek = liryka, epika albo dramat) i wypozyczenia (58 wypożyczeń, id_ks to klucz obcy do ksiazka.id).

  3. Krok 3: Zrzut ekranu import.png

    Kryteria:

    R.1.1 Wykonany import tabeli do bazy danych biblioteka , czynność udokumentowana w pliku 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ń.

    Cały ekran z paskiem zadań, zapis jako import.png. Zrzuty zapytań w tym arkuszu są w JPEG.

    Typowe błędy

    Wykadrowany zrzut nie jest oceniany.

  4. Krok 4: Zapytanie 1: książki z gatunku liryka

    Kryteria: ,

    R.1.2 Zapisany plik kwerendy.txt zawierający co najmniej jedno zapytanie SQL, wynikające z treści zadania

    R.1.3 Wykonane zapytanie 1 wybierające jedynie id i tytuły książek z gatunku liryka. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT id, tytul FROM ksiazka WHERE gatunek = "liryka"; oraz wynik działania zapytania udokumentowany zrzutem, na którym widocznych jest dokładnie 5 rekordów z id: 1, 5, 6, 19, 20

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 1: wybierające jedynie id i tytuły książek z gatunku liryka.

    Każda klauzula w osobnej linii, napis w apostrofach - to zapis ze standardu SQL, działający w każdej bazie (cudzysłów jako napis rozumie tylko MySQL). Wynik: 5 książek, id 1, 5, 6, 19, 20 (R.1.3). Zapytanie do kwerendy.txt, zrzut kw1.jpg. To samo zapytanie, z gatunkiem jako parametrem, wypełni trzy listy rozwijane.

    kwerendy.txt Tekst nowy plik
    -- Zapytanie 1
    SELECT id, tytul
    FROM ksiazka
    WHERE gatunek = 'liryka';
    Podgląd strony po tym kroku
    Podgląd strony po kroku 4
  5. Krok 5: Zapytanie 2: piętnaście najwcześniejszych oddań

    Kryteria:

    R.1.4 Wykonane zapytanie 2 wybierające pierwsze 15 rekordów, jedynie tytuł książki oraz odpowiadające mu id czytelnika i data oddania posortowane rosnąco według daty oddania. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT tytul, id_cz, data_odd FROM ksiazka JOIN wypozyczenia ON id = id_ks ORDER BY data_odd LIMIT 15; (możliwe również INNER JOIN lub porównanie kluczy po WHERE) oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczne jest dokładnie 15 rekordów, pierwsze 5 rekordów o id_cz: 1, 2, 1, 4, 4

    ok. 4 min
    Polecenie z arkusza

    Zapytanie 2: wybierające pierwsze 15 rekordów, jedynie tytuł książki oraz odpowiadające mu id czytelnika i data oddania posortowane rosnąco według daty oddania. Należy posłużyć się relacją.

    Złączenie z aliasami: ksiazka AS k, wypozyczenia AS w, warunek ON w.id_ks = k.id (klucz obcy = klucz główny). Kolejność klauzul jest stała: FROM → JOIN → ORDER BY → LIMIT - najpierw sortowanie, potem odcięcie piętnastu pierwszych.

    Wynik: 15 wierszy, pierwsze pięć z czytelnikami 1, 2, 1, 4, 4 (R.1.4), zrzut kw2.jpg.

    Typowe błędy

    LIMIT 15 bez ORDER BY zwraca piętnaście dowolnych wierszy.

    kwerendy.txt Tekst zmienione: 7 linii
    -- Zapytanie 1
    SELECT id, tytul
    FROM ksiazka
    WHERE gatunek = 'liryka';
    
    -- Zapytanie 2
    SELECT k.tytul, w.id_cz, w.data_odd
    FROM ksiazka AS k
    JOIN wypozyczenia AS w ON w.id_ks = k.id
    ORDER BY w.data_odd ASC
    LIMIT 15;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 5
  6. Krok 6: Zapytanie 3: nowa kolumna rezerwacja

    Kryteria:

    R.1.5 Wykonane zapytanie 3 modyfikujące strukturę tabeli ksiazka . Dodana jest nowa kolumna o nazwie rezerwacja typu jednocyfrowego TINYINT, przyjmująca wartość domyślną 0. W pliku z kwerendami lub na zrzucie istnieje zapis np.: ALTER TABLE ksiazka ADD COLUMN rezerwacja TINYINT(1) DEFAULT 0; (COLUMN opcjonalne) oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczny jest komunikat o poprawnym wykonaniu zapytania lub zmieniona struktura tabeli

    ok. 3 min
    Polecenie z arkusza

    Zapytanie 3: modyfikujące strukturę tabeli ksiazka, poprzez dodanie nowej kolumny o nazwie rezerwacja typu TINYINT, o długości 1 Bajta, która przyjmuje wartość domyślną 0 (oznacza, że książka nie jest zarezerwowana).

    ALTER TABLE … ADD COLUMN zmienia strukturę. Typ TINYINT zajmuje zawsze 1 bajt (zakres -128…127) - to on spełnia „długość 1 bajta”. Liczba w nawiasie, TINYINT(1), to tylko szerokość wyświetlania (podpowiedź dla programów, ile cyfr pokazać), nie rozmiar; zostaje, bo tak zapisuje to klucz.

    NOT NULL DEFAULT 0: każda książka, także dodana później bez podania tej kolumny, ma 0, a nie pusty NULL - a rezerwacja ma tylko dwa stany. Zrzut kw3.jpg (R.1.5). Bez tej kolumny skrypt 3 nie zadziała.

    Typowe błędy

    UPDATE zamiast ALTER TABLE zmienia dane, nie strukturę - kolumny nie przybędzie.

    kwerendy.txt Tekst zmienione: 3 linie
    -- Zapytanie 1
    SELECT id, tytul
    FROM ksiazka
    WHERE gatunek = 'liryka';
    
    -- Zapytanie 2
    SELECT k.tytul, w.id_cz, w.data_odd
    FROM ksiazka AS k
    JOIN wypozyczenia AS w ON w.id_ks = k.id
    ORDER BY w.data_odd ASC
    LIMIT 15;
    
    -- Zapytanie 3
    ALTER TABLE ksiazka ADD COLUMN rezerwacja TINYINT(1) NOT NULL DEFAULT 0;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 6
  7. Krok 7: Zapytanie 4: rezerwacja książki o id 1

    Kryteria:

    R.1.6 Wykonane zapytanie 4 aktualizujące w tabeli ksiazka pole rezerwacja na wartość 1 dla id = 1. W pliku z kwerendami lub na zrzucie istnieje zapis np.: UPDATE ksiazka SET rezerwacja = 1 WHERE id = 1;

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 4: aktualizujące pole rezerwacja na wartość 1 dla książki, której id=1.

    UPDATE zmienia dane: SET mówi co, WHERE - w którym wierszu (R.1.6). Zrzut kw4.jpg z komunikatem o jednym zmienionym wierszu.

    Typowe błędy

    UPDATE bez WHERE rezerwuje wszystkie 20 książek.

    kwerendy.txt Tekst zmienione: 5 linii
    -- Zapytanie 1
    SELECT id, tytul
    FROM ksiazka
    WHERE gatunek = 'liryka';
    
    -- Zapytanie 2
    SELECT k.tytul, w.id_cz, w.data_odd
    FROM ksiazka AS k
    JOIN wypozyczenia AS w ON w.id_ks = k.id
    ORDER BY w.data_odd ASC
    LIMIT 15;
    
    -- Zapytanie 3
    ALTER TABLE ksiazka ADD COLUMN rezerwacja TINYINT(1) NOT NULL DEFAULT 0;
    
    -- Zapytanie 4
    UPDATE ksiazka
    SET rezerwacja = 1
    WHERE id = 1;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 7
  8. Krok 8: Zapytanie 5: tytuł książki o id 4

    Kryteria:

    R.1.7 Wykonane zapytanie 5 wybierające jedynie tytuł książki dla id = 4. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT tytul FROM ksiazka WHERE id = 4; oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczny jest tytuł Antygona

    ok. 1 min
    Polecenie z arkusza

    Zapytanie 5: wybierające jedynie tytuł książki, której id=4.

    Jedna kolumna, jeden wiersz po kluczu głównym - Antygona (R.1.7), zrzut kw5.jpg.

    kwerendy.txt Tekst zmienione: 5 linii
    -- Zapytanie 1
    SELECT id, tytul
    FROM ksiazka
    WHERE gatunek = 'liryka';
    
    -- Zapytanie 2
    SELECT k.tytul, w.id_cz, w.data_odd
    FROM ksiazka AS k
    JOIN wypozyczenia AS w ON w.id_ks = k.id
    ORDER BY w.data_odd ASC
    LIMIT 15;
    
    -- Zapytanie 3
    ALTER TABLE ksiazka ADD COLUMN rezerwacja TINYINT(1) NOT NULL DEFAULT 0;
    
    -- Zapytanie 4
    UPDATE ksiazka
    SET rezerwacja = 1
    WHERE id = 1;
    
    -- Zapytanie 5
    SELECT tytul
    FROM ksiazka
    WHERE id = 4;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 8
  9. Etap 2: Grafika i strona HTML

    Kroki 9-22 · po tym etapie masz 20 z 42 kryteriów (pkt)

    Krok 9: Grafika obraz.png: wysokość 200 px, przezroczyste tło

    Kryteria:

    R.2.1 Grafika obraz.png ma tło przezroczyste oraz została przeskalowana z zachowaniem proporcji do wysokości 200 px, odpowiadająca temu szerokość wynosi 188 px (± 5 px)

    ok. 5 min
    Polecenie z arkusza

    Cechy grafiki obraz.jpg: przeskalowana z zachowaniem proporcji do wysokości 200 px. Przezroczystość zamiast białego tła. Zapisana pod tą samą nazwą, w formacie PNG.

    W GIMP-ie: Warstwa → Przezroczystość → Dodaj kanał alfa, różdżka na białym tle i Delete, Skaluj obraz do wysokości 200 px przy zamkniętym łańcuchu (szerokość 188 px), Wyeksportuj jako obraz.png (R.2.1). PNG, bo JPEG nie przechowuje przezroczystości.

    Typowe błędy

    Eksport do .jpg przywraca białe tło.

    Podgląd strony po tym kroku
    Podgląd strony po kroku 9
  10. Krok 10: Plik biblioteka.php w HTML5

    Kryteria:

    R.2.2 W pliku biblioteka.php zapisana deklaracja HTML5 <!DOCTYPE HTML>, deklaracja języka zawartości: polski np. <html lang="pl">, w sekcji head znajduje się jawnie podany standard kodowania polskich znaków

    ok. 1 min
    Polecenie z arkusza

    Składa się ze strony o nazwie biblioteka.php. Zapisana w języku HTML5.

    Plik biblioteka.php w folderze zdającego. <!DOCTYPE html> włącza tryb standardów HTML5, dalej szkielet: <html>, <head> i <body>. Rozszerzenie .php, bo na początku pliku stanie kod PHP.

    biblioteka.php PHP nowy plik
    <!DOCTYPE html>
    <html>
    <head>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  11. Krok 11: Język strony

    Kryteria:

    R.2.2 W pliku biblioteka.php zapisana deklaracja HTML5 <!DOCTYPE HTML>, deklaracja języka zawartości: polski np. <html lang="pl">, w sekcji head znajduje się jawnie podany standard kodowania polskich znaków

    ok. 1 min
    Polecenie z arkusza

    Zadeklarowany polski język zawartości witryny.

    lang="pl" w <html> - czytnik ekranu czyta polską wymową, przeglądarka dobiera słownik i dzielenie wyrazów.

    biblioteka.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  12. Krok 12: Kodowanie polskich znaków

    Kryteria:

    R.2.2 W pliku biblioteka.php zapisana deklaracja HTML5 <!DOCTYPE HTML>, deklaracja języka zawartości: polski np. <html lang="pl">, w sekcji head znajduje się jawnie podany standard kodowania polskich znaków

    ok. 1 min
    Polecenie z arkusza

    Jawnie zastosowany właściwy standard kodowania polskich znaków.

    <meta charset="UTF-8"> jako pierwszy element <head>. Ten sam standard ustawi połączenie z bazą (charset=utf8mb4 w PDO), więc polskie litery przejdą bez zmian całą drogę: baza → PHP → przeglądarka.

    biblioteka.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  13. Krok 13: Tytuł strony

    Kryteria:

    R.2.3 Nadany tytuł stronie "Biblioteka miejska”

    ok. 1 min
    Polecenie z arkusza

    Tytuł strony „Biblioteka miejska”.

    <title>Biblioteka miejska</title> - napis na karcie przeglądarki. Obok <meta name="viewport">: bez niego telefon pokazałby stronę pomniejszoną jak na monitorze. Arkusz tego nie wymaga, a w każdym współczesnym szablonie ta linia jest.

    biblioteka.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  14. Krok 14: Podpięcie arkusza stylów

    Kryteria:

    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

    ok. 1 min
    Polecenie z arkusza

    Arkusz stylów w pliku o nazwie styl.css prawidłowo połączony z kodem strony.

    <link rel="stylesheet" href="styl.css"> - nazwa dokładnie z arkusza. Cały wygląd będzie w tym pliku, w HTML nie ma ani jednego atrybutu style.

    Typowe błędy

    Zła nazwa w href to brak stylów, a wtedy cały rezultat R.4 przepada.

    biblioteka.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  15. Krok 15: Bloki semantyczne

    Kryteria:

    R.2.4 W sekcji body zdefiniowany jest podział na bloki za pomocą semantycznych znaczników HTML 5: header, section+section+section+section, footer

    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.

    Ilustracja 3 nie ma bloku głównego: nagłówek, cztery bloki sekcji, stopka - dokładnie znaczniki z klucza R.2.4. Wszystkie bloki są bezpośrednio w <body>, więc to <body> stanie się siatką w CSS. Sekcje nie potrzebują identyfikatorów - wszystkie wyglądają tak samo.

    Typowe błędy

    Cztery <div> wyglądają tak samo, ale R.2.4 wymaga znaczników semantycznych.

    biblioteka.php PHP zmienione: 12 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
        </section>
        <section>
        </section>
        <section>
        </section>
        <section>
        </section>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  16. Krok 16: Blok nagłówkowy: miejsce na skrypt 1

    Kryteria:

    R.2.6 W bloku nagłówkowym umieszczona przynajmniej jedna grafika obraz.png (w HTML lub skrypcie)

    ok. 1 min
    Polecenie z arkusza

    Zawartość bloku nagłówkowego: efekt działania skryptu 1.

    Nagłówek zostaje pusty - dwadzieścia grafik wstawi skrypt 1 w etapie PHP.

    Podgląd strony bez zmian - taki jak po kroku 15.

  17. Krok 17: Pierwsza sekcja: nagłówek „Liryka”

    Kryteria:

    R.2.5 Umieszczone w blokach sekcji nagłówki <h2>: „Liryka”, „Epika”, „Dramat”, „Zaległe książki” oraz paragraf z numerem zdającego w stopce

    ok. 1 min
    Polecenie z arkusza

    Zawartość pierwszego bloku sekcji: nagłówek drugiego stopnia o treści „Liryka”.

    <h2> w pierwszej sekcji (R.2.5).

    biblioteka.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
        </section>
        <section>
        </section>
        <section>
        </section>
        <section>
        </section>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  18. Krok 18: Pierwsza sekcja: formularz z listą rozwijaną

    Kryteria: , ,

    R.2.7 W przynajmniej jednym bloku umieszczony formularz

    R.2.8 W przynajmniej jednym bloku umieszczona lista rozwijana i przycisk "Rezerwuj", do listy zastosowane znaczniki <select> i <option> (w HTML lub skrypcie)

    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 metodą bezpieczną do tego samego pliku, z elementami: lista rozwijana wypełniona skryptem 2.

    method="post" bez action (R.2.7, R.3.3). Lista ma nazwę poetry - każda sekcja dostanie inną (poetry, epic, drama), więc PHP rozpozna, z której sekcji przyszło zamówienie. Arkusz nie daje liście etykiety, dlatego aria-label="Książka": czytnik ekranu przeczyta, czego dotyczy lista.

    Typowe błędy

    Ta sama nazwa listy w trzech formularzach uniemożliwia wskazanie sekcji, w której pokazać komunikat (R.5.8).

    biblioteka.php PHP zmienione: 4 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
            </form>
        </section>
        <section>
        </section>
        <section>
        </section>
        <section>
        </section>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  19. Krok 19: Pierwsza sekcja: przycisk „Rezerwuj”

    Kryteria: ,

    R.2.8 W przynajmniej jednym bloku umieszczona lista rozwijana i przycisk "Rezerwuj", do listy zastosowane znaczniki <select> i <option> (w HTML lub skrypcie)

    R.3.4 Przynajmniej jeden przycisk realizuje funkcję wysłania danych z formularza (input lub button z typem submit lub button bez typu)

    ok. 1 min
    Polecenie z arkusza

    Przycisk „Rezerwuj” wysyłający dane z formularza do skryptu 3.

    <button type="submit"> w formularzu (R.2.8, R.3.4). Komunikat skryptu 3 stanie pod formularzem.

    biblioteka.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
        </section>
        <section>
        </section>
        <section>
        </section>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  20. Krok 20: Druga i trzecia sekcja: Epika i Dramat

    Kryteria:

    R.2.5 Umieszczone w blokach sekcji nagłówki <h2>: „Liryka”, „Epika”, „Dramat”, „Zaległe książki” oraz paragraf z numerem zdającego w stopce

    ok. 2 min
    Polecenie z arkusza

    Zawartość drugiego bloku sekcji: nagłówek drugiego stopnia o treści „Epika”, formularz z listą rozwijaną i przyciskiem „Rezerwuj”. Zawartość trzeciego bloku sekcji: nagłówek drugiego stopnia o treści „Dramat”, formularz z listą rozwijaną i przyciskiem „Rezerwuj”.

    Ta sama budowa z innym nagłówkiem i inną nazwą listy: epic, drama (R.2.5).

    biblioteka.php PHP zmienione: 12 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
        </section>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  21. Krok 21: Czwarta sekcja: nagłówek i lista

    Kryteria: ,

    R.2.5 Umieszczone w blokach sekcji nagłówki <h2>: „Liryka”, „Epika”, „Dramat”, „Zaległe książki” oraz paragraf z numerem zdającego w stopce

    R.2.9 W czwartym bloku sekcji umieszczona jedna lista punktowana z elementami (w HTML lub skrypcie) , zastosowane znaczniki <ul> i <li>

    ok. 1 min
    Polecenie z arkusza

    Zawartość czwartego bloku sekcji: nagłówek drugiego stopnia o treści „Zaległe książki”. Lista punktowana (nieuporządkowana) wypełniona skryptem 4.

    <h2> i pusta <ul> - elementy <li> wypisze skrypt 4 (R.2.5, R.2.9).

    biblioteka.php PHP zmienione: 3 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  22. Krok 22: Stopka z numerem zdającego

    Kryteria: ,

    R.2.5 Umieszczone w blokach sekcji nagłówki <h2>: „Liryka”, „Epika”, „Dramat”, „Zaległe książki” oraz paragraf z numerem zdającego w stopce

    R.2.10 Zastosowany znacznik <strong> do zapisu treści stopki

    ok. 1 min
    Polecenie z arkusza

    Zawartość stopki: paragraf o treści „Autor: ”, dalej wstawiony numer zdającego. Tekst w paragrafie jest zapisany za pomocą znacznika semantycznego oznaczającego tekst ważny, formatowany domyślnie jako pogrubiony.

    Cała treść paragrafu w <strong> (R.2.10). Tu nazwisko autora rozwiązania - na egzaminie wpisujesz swój numer zdającego (PESEL), bo tego sprawdza klucz R.2.5.

    Typowe błędy

    <b> pogrubia, ale nie jest znacznikiem semantycznym.

    biblioteka.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  23. Etap 3: Styl CSS

    Kroki 23-30 · po tym etapie masz 32 z 42 kryteriów (pkt)

    Krok 23: Plik styl.css: krój czcionki

    Kryteria:

    R.4.1 Ustawione domyślnie dla wszystkich selektorów (*): krój czcionki Cambria

    ok. 1 min
    Polecenie z arkusza

    Domyślnie dla wszystkich selektorów: krój czcionki Cambria.

    Selektor uniwersalny * z krojem Cambria (R.4.1).

    styl.css CSS nowy plik
    * {
        font-family: Cambria;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  24. Krok 24: Ciało strony: kolor tła

    Kryteria:

    R.4.3 Ustawiony dla selektora body kolor tła MistyRose

    ok. 1 min
    Polecenie z arkusza

    Dla selektora ciała strony: kolor tła MistyRose.

    Selektor znacznika body (uwaga arkusza), kolor nazwany MistyRose (R.4.3).

    styl.css CSS zmienione: 4 linie
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  25. Krok 25: Nagłówek i stopka: wysokość, wyrównanie, ukrywanie nadmiaru

    Kryteria: , ,

    R.4.4 Ustawiona wysokość bloku nagłówkowego i stopki 200px oraz wszystkich bloków sekcji 400px

    R.4.5 Ustawione wyrównanie tekstu do środka dla bloku nagłówkowego i stopki oraz selektora h2

    R.4.7 Ustawiona dla bloków nagłówkowego, stopki, wszystkich sekcji cecha overflow: hidden;

    ok. 2 min
    Polecenie z arkusza

    Dla bloku nagłówkowego i stopki: wysokość 200 px, wyrównanie tekstu do środka, w przypadku przepełnienia bloku zawartość poza blokiem jest ukrywana.

    overflow: hidden obcina to, co się nie mieści - z dwudziestu grafik nagłówek pokaże jeden rząd (R.4.4, R.4.5, R.4.7).

    styl.css CSS zmienione: 7 linii
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
    }
    
    header,
    footer {
        height: 200px;
        text-align: center;
        overflow: hidden;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  26. Krok 26: Bloki sekcji: tło, wysokość, marginesy, cień, ukrywanie nadmiaru

    Kryteria: , , , ,

    R.4.2 Ustawiony dla wszystkich bloków sekcji kolor tła Plum

    R.4.4 Ustawiona wysokość bloku nagłówkowego i stopki 200px oraz wszystkich bloków sekcji 400px

    R.4.6 Ustawione marginesy zewnętrzne (margin) dla wszystkich bloków sekcji 5px

    R.4.7 Ustawiona dla bloków nagłówkowego, stopki, wszystkich sekcji cecha overflow: hidden;

    R.4.8 Ustawiona dla wszystkich bloków sekcji cecha box-shadow: 4px 4px 10px DimGray;

    ok. 2 min
    Polecenie z arkusza

    Dla bloków sekcji: kolor tła Plum, wysokość 400 px, marginesy zewnętrzne 5 px, cień bloku o przesunięciu 4 px w obu osiach, rozmyciu 10 px i kolorze DimGray, w przypadku przepełnienia bloku zawartość poza blokiem jest ukrywana.

    Jedna reguła na cztery sekcje; cień bloku z tabeli 2: box-shadow: 4px 4px 10px DimGray (R.4.2, R.4.4, R.4.6, R.4.7, R.4.8).

    Typowe błędy

    text-shadow to cień liter, nie bloku.

    styl.css CSS zmienione: 8 linii
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
    }
    
    header,
    footer {
        height: 200px;
        text-align: center;
        overflow: hidden;
    }
    
    section {
        background-color: Plum;
        height: 400px;
        margin: 5px;
        box-shadow: 4px 4px 10px DimGray;
        overflow: hidden;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  27. Krok 27: Wąskie ekrany: siatka o jednej kolumnie

    Kryteria:

    R.3.5 Sekcje są umiejscowione obok siebie dla ekranu o szerokości powyżej 800 px; dla ekranu o szerokości mniejszej niż 800 px, sekcje są umiejscowione jedna pod drugą

    ok. 2 min
    Polecenie z arkusza

    W przypadku ekranów o szerokości do 800 px szerokość bloków sekcji 100%, powyżej 800 px szerokość bloków sekcji 22%.

    Podejście „mobile first”: najpierw układ dla wąskiego ekranu, bez żadnego @media. <body> staje się siatką z jedną kolumną 100% - każdy blok w osobnym wierszu, sekcje jedna pod drugą (prawa część ilustracji 2). Nagłówek i stopka dostają grid-column: 1 / -1: „od pierwszej do ostatniej linii siatki”, czyli zawsze całą szerokość, ile kolumn by nie było.

    Dlaczego od wąskiego: telefon nie pobiera i nie przelicza reguł dla dużego ekranu, a szeroki układ to dodatek, który włącza się dopiero, gdy jest miejsce.

    styl.css CSS zmienione: 7 linii
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
        display: grid;
        grid-template-columns: 100%;
    }
    
    header,
    footer {
        grid-column: 1 / -1;
    }
    
    header,
    footer {
        height: 200px;
        text-align: center;
        overflow: hidden;
    }
    
    section {
        background-color: Plum;
        height: 400px;
        margin: 5px;
        box-shadow: 4px 4px 10px DimGray;
        overflow: hidden;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  28. Krok 28: Szerokie ekrany: cztery kolumny po 22%

    Kryteria: , ,

    R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z ilustracją 3 w arkuszu egzaminacyjnym dla ekranów o szerokości powyżej 800 px (prawidłowo zastosowane 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.3.5 Sekcje są umiejscowione obok siebie dla ekranu o szerokości powyżej 800 px; dla ekranu o szerokości mniejszej niż 800 px, sekcje są umiejscowione jedna pod drugą

    R.4.9 W przypadku ekranów o szerokości do 800 px szerokość bloków sekcji 100%, powyżej 800 px szerokość bloków sekcji 22%, np. @media only screen and (max-width: 800px) { section { width:100%; } } (wystarczy jedna reguła @media, wtedy szerokość w sekcji wymagana, obie szerokości 100% i 22% muszą być zdefiniowane)

    ok. 3 min
    Polecenie z arkusza

    W przypadku ekranów o szerokości do 800 px szerokość bloków sekcji 100%, powyżej 800 px szerokość bloków sekcji 22%.

    „Powyżej 800 px” to @media (min-width: 801px). W środku zmienia się tylko jedna właściwość: siatka dostaje cztery kolumny po 22% (repeat(4, 22%)) i piątą 1fr na resztę szerokości. Sekcje wypełniają kolejno cztery kolumny, a nagłówek i stopka z 1 / -1 rozciągają się na wszystkie pięć - układ z ilustracji 3, bez float i bez clear (R.3.1, R.3.5).

    Obie szerokości z arkusza - 100% i 22% - są zapisane w siatce (R.4.9). Margines 5 px sekcji mieści się w kolumnie, więc ramka sekcji jest o 10 px węższa niż kolumna.

    Typowe błędy

    Klucz R.4.9 podaje przykład z width w sekcji. Jeśli wolisz trzymać się dokładnie tego zapisu, zostaw section { width: 22%; } z @media jak w wersji podstawowej - w siatce width w procentach liczyłby się od kolumny, a nie od strony, więc obu sposobów naraz się nie łączy.

    styl.css CSS zmienione: 6 linii
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
        display: grid;
        grid-template-columns: 100%;
    }
    
    header,
    footer {
        grid-column: 1 / -1;
    }
    
    header,
    footer {
        height: 200px;
        text-align: center;
        overflow: hidden;
    }
    
    section {
        background-color: Plum;
        height: 400px;
        margin: 5px;
        box-shadow: 4px 4px 10px DimGray;
        overflow: hidden;
    }
    
    @media (min-width: 801px) {
        body {
            grid-template-columns: repeat(4, 22%) 1fr;
        }
    }
    Podgląd strony po tym kroku (biblioteka.php)
  29. Krok 29: Nagłówek drugiego stopnia: wyrównanie

    Kryteria:

    R.4.5 Ustawione wyrównanie tekstu do środka dla bloku nagłówkowego i stopki oraz selektora h2

    ok. 1 min
    Polecenie z arkusza

    Dla selektora nagłówka drugiego stopnia: wyrównanie tekstu do środka.

    Selektor znacznika h2 (R.4.5).

    styl.css CSS zmienione: 4 linie
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
        display: grid;
        grid-template-columns: 100%;
    }
    
    header,
    footer {
        grid-column: 1 / -1;
    }
    
    header,
    footer {
        height: 200px;
        text-align: center;
        overflow: hidden;
    }
    
    section {
        background-color: Plum;
        height: 400px;
        margin: 5px;
        box-shadow: 4px 4px 10px DimGray;
        overflow: hidden;
    }
    
    @media (min-width: 801px) {
        body {
            grid-template-columns: repeat(4, 22%) 1fr;
        }
    }
    
    h2 {
        text-align: center;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  30. Krok 30: Przycisk pod kursorem

    Kryteria:

    R.4.10 Gdy kursor znajdzie się na dowolnym przycisku jego kolor tła zmienia się na #505050 a kolor czcionki na biały

    ok. 1 min
    Polecenie z arkusza

    Gdy kursor znajdzie się na dowolnym przycisku jego kolor tła zmienia się na #505050 a kolor czcionki na biały.

    button:hover - dowolny przycisk na stronie (R.4.10).

    styl.css CSS zmienione: 5 linii
    * {
        font-family: Cambria;
    }
    
    body {
        background-color: MistyRose;
        display: grid;
        grid-template-columns: 100%;
    }
    
    header,
    footer {
        grid-column: 1 / -1;
    }
    
    header,
    footer {
        height: 200px;
        text-align: center;
        overflow: hidden;
    }
    
    section {
        background-color: Plum;
        height: 400px;
        margin: 5px;
        box-shadow: 4px 4px 10px DimGray;
        overflow: hidden;
    }
    
    @media (min-width: 801px) {
        body {
            grid-template-columns: repeat(4, 22%) 1fr;
        }
    }
    
    h2 {
        text-align: center;
    }
    
    button:hover {
        background-color: #505050;
        color: white;
    }
    Podgląd strony po tym kroku (biblioteka.php)
  31. Etap 4: Skrypty PHP

    Kroki 31-45 · po tym etapie masz 42 z 42 kryteriów (pkt)

    Krok 31: Dane połączenia w pliku config.php

    Kryteria:

    R.5.1 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy biblioteka

    ok. 2 min
    Polecenie z arkusza

    Łączą się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie biblioteka.

    Dane logowania do bazy w osobnym pliku config.php, który tylko zwraca tablicę (return [...]). W prawdziwym projekcie hasło do bazy jest inne na każdym serwerze i nie trafia do repozytorium razem z kodem strony - wystarczy podmienić jeden plik. Arkusz dopuszcza dodatkowe pliki („ewentualnie inne przygotowane pliki”).

    DSN (mysql:host=localhost;dbname=biblioteka;charset=utf8mb4) to adres bazy dla PDO w jednym napisie: rodzaj serwera, host, nazwa bazy i kodowanie połączenia. Plik bez HTML-u nie ma końcowego ?> - nic po nim nie wypisze się przypadkiem.

    Typowe błędy

    Plik config.php musi trafić do folderu z rezultatami - bez niego strona się nie połączy, a egzaminator nie znajdzie nazwy bazy.

    config.php PHP nowy plik
    <?php
    // Dane połączenia z bazą - w jednym miejscu, poza plikiem strony
    return [
        'dsn' => 'mysql:host=localhost;dbname=biblioteka;charset=utf8mb4',
        'user' => 'root',
        'password' => '',
    ];

    Podgląd strony bez zmian - taki jak po kroku 30.

  32. Krok 32: Połączenie z bazą przez PDO

    Kryteria: ,

    R.5.1 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy biblioteka

    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

    Napisane w języku PHP. Łączą się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie biblioteka.

    Kod PHP stoi na samym początku pliku, przed <!DOCTYPE html>: najpierw wszystkie operacje na bazie, potem szablon strony, który tylko wypisuje gotowe dane. Klucz R.5 przyjmuje PDO tak samo jak MySQLi.

    • require 'config.php' - dane połączenia trafiają do $config,
    • new PDO(...) - połączenie i wybór bazy; opcja ERRMODE_EXCEPTION sprawia, że każdy błąd SQL zatrzymuje skrypt wyjątkiem, zamiast po cichu zwracać false, a FETCH_ASSOC - że wiersze przychodzą jako tablice z nazwami kolumn ($row['nazwa'] zamiast $row[1]),
    • try … catch (PDOException $error) - gdy baza nie działa, użytkownik widzi zwykły komunikat, a nie treść błędu z nazwą serwera i ścieżkami; http_response_code(500) mówi przeglądarce, że to błąd serwera.

    Od tego kroku podgląd to prawdziwy wynik strony z serwera - na razie bez zmian.

    Typowe błędy

    catch wypisujący $error->getMessage() pokazałby użytkownikowi szczegóły serwera.

    biblioteka.php PHP zmienione: 13 linii
    <?php
    $config = require 'config.php';
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  33. Krok 33: Funkcja e(): bezpieczne wypisywanie tekstu

    ok. 2 min
    Polecenie z arkusza

    Należy stosować znaczące nazewnictwo zmiennych i funkcji w języku polskim lub angielskim.

    Każdy napis z bazy albo z formularza, który trafi do HTML-u, przejdzie przez e() (od escape). htmlspecialchars zamienia <, >, & i - dzięki ENT_QUOTES - oba cudzysłowy na encje, więc napis <script> zapisany kiedyś w bazie wyświetli się jako tekst, a nie wykona (ochrona przed XSS). Escapowanie robi się na wyjściu, tuż przed wypisaniem - w bazie zostają dane surowe.

    Krótka nazwa to świadomy wyjątek od „znaczącego nazewnictwa”: e() występuje w szablonie wiele razy i jest powszechnie znaną konwencją (tak samo nazywa się w Laravelu). Typy string przy parametrze i wyniku mówią, czego funkcja oczekuje; liczby PHP zamieni na tekst sam.

    biblioteka.php PHP zmienione: 5 linii
    <?php
    $config = require 'config.php';
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 32.

  34. Krok 34: Skrypt 1: dwadzieścia grafik w pętli

    Kryteria: ,

    R.2.6 W bloku nagłówkowym umieszczona przynajmniej jedna grafika obraz.png (w HTML lub skrypcie)

    R.5.3 Skrypt 1 wyświetla 20 razy grafikę za pomocą pętli (w zależności od szerokości ekranu grafiki mogą być ukryte)

    ok. 2 min
    Polecenie z arkusza

    Skrypt 1: wyświetla 20 razy grafikę obraz.png. Należy zastosować instrukcję iteracyjną (pętlę).

    Pętla for w składni szablonu (for (...): … endfor;) - między nimi zwykły HTML obrazu, powtórzony 20 razy (licznik od 0 do 19, R.5.3). Wynik jest ten sam co echo w pętli, ale znacznik <img> zostaje HTML-em, a nie napisem w PHP.

    Pusty alt="": dwadzieścia identycznych obrazów to dekoracja, nie treść. Pusty tekst alternatywny mówi czytnikowi ekranu „pomiń”; alt="książki" kazałby mu przeczytać słowo „książki” dwadzieścia razy. Arkusz nie podaje tekstu alternatywnego dla tej grafiki.

    Typowe błędy

    str_repeat('<img …>', 20) daje ten sam HTML, ale arkusz wymaga instrukcji iteracyjnej - funkcja nie jest pętlą w kodzie zdającego (R.5.3).

    biblioteka.php PHP zmienione: 3 linie
    <?php
    $config = require 'config.php';
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  35. Krok 35: Skrypt 2: zapytanie 1 do bazy

    Kryteria:

    R.5.4 Przynajmniej jeden skrypt zawiera instrukcję wysyłającą do bazy danych zapytanie

    ok. 3 min
    Polecenie z arkusza

    Skrypt 2 w sekcji „Liryka”: wysyła do bazy danych zapytanie 1.

    Zapytanie 1 jako zapytanie przygotowane z parametrem :genre - za chwilę to samo zapytanie posłuży trzem gatunkom. execute(['genre' => 'liryka']) wysyła je z wartością (R.5.4).

    PDO::FETCH_KEY_PAIR zamienia wynik z dwiema kolumnami w tablicę „klucz → wartość”: [1 => 'Pieśń nad pieśniami', 5 => 'Bogurodzica', …]. Id i tytuł są dokładnie tym, czego potrzebuje opcja listy (id w value, tytuł jako treść).

    biblioteka.php PHP zmienione: 4 linie
    <?php
    $config = require 'config.php';
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $booksQuery->execute(['genre' => 'liryka']);
        $poetryBooks = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 34.

  36. Krok 36: Skrypt 2: opcje z tytułem i id

    Kryteria:

    R.5.5 Skrypt 2 w przynajmniej jednej sekcji wyświetla tytuły książek zwrócone zapytaniem. Rodzaj utworów zgadza się z nazwą sekcji (liryka, epika, dramat) . Atrybut value każdej opcji to id zwrócone zapytaniem

    ok. 2 min
    Polecenie z arkusza

    Ze zwróconych zapytaniem wierszy generuje kolejne opcje listy rozwijanej w ten sposób, że tytuł jest treścią opcji, a id jest przypisane do atrybutu value.

    foreach ($poetryBooks as $id => $title) od razu rozdziela klucz i wartość: id trafia do value, tytuł (przez e()) jest treścią opcji (R.5.5). Formularz wyśle id, a użytkownik widzi tytuł. Id to liczba z bazy - escapowanie nie jest potrzebne. Lista Liryka ma pięć tytułów, jak na ilustracji 4.

    Typowe błędy

    Opcja bez value wyśle tytuł zamiast id - skrypt 3 nie znajdzie książki po id.

    biblioteka.php PHP zmienione: 3 linie
    <?php
    $config = require 'config.php';
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $booksQuery->execute(['genre' => 'liryka']);
        $poetryBooks = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($poetryBooks as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  37. Krok 37: Skrypt 2 dla trzech gatunków: pętla zamiast kopii

    Kryteria: ,

    R.5.5 Skrypt 2 w przynajmniej jednej sekcji wyświetla tytuły książek zwrócone zapytaniem. Rodzaj utworów zgadza się z nazwą sekcji (liryka, epika, dramat) . Atrybut value każdej opcji to id zwrócone zapytaniem

    R.5.8 Skrypty 2 i 3 działają we wszystkich trzech sekcjach zgadza się gatunek "Liryka", "Epika", "Dramat" z wyświetlanymi w danej liście tytułami oraz tekst pojawia się tylko pod listą w której był dokonany wybór

    ok. 3 min
    Polecenie z arkusza

    Skrypt 2 do sekcji „Epika” należy powielić i dostosować: działa on na zmodyfikowanym zapytaniu 1, tak że wypełnia listę rozwijaną książkami z gatunku „epika”. Skrypt 2 do sekcji „Dramat” należy powielić i dostosować: wypełnia listę rozwijaną książkami z gatunku „dramat”.

    Zamiast trzech kopii skryptu - tablica sekcji na początku pliku: $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'], czyli nazwa listy w formularzu → gatunek w bazie. Pętla po niej wykonuje to samo przygotowane zapytanie trzy razy, za każdym razem z innym gatunkiem: zapytanie przygotowuje się raz, a wykonuje wiele razy. Wyniki lądują w $books['liryka'], $books['epika'], $books['dramat'].

    W HTML-u każda lista bierze swoją część tablicy. Zmiana zapytania to teraz jedno miejsce zamiast trzech; klucz R.5.8 sprawdza, czy w każdej sekcji są książki jej gatunku (8 epiki, 7 dramatów) - i są.

    Typowe błędy

    Gdyby arkusz sprawdzał trzy kopie kodu, a nie działanie, pętla byłaby ryzykiem - tu klucz ocenia wynik w trzech sekcjach (R.5.5, R.5.8).

    biblioteka.php PHP zmienione: 13 linii
    <?php
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  38. Krok 38: Skrypt 3: tylko gdy wybrano książkę z listy liryki

    ok. 3 min
    Polecenie z arkusza

    Skrypt 3 (ilustracja 5), jeżeli dokonano wyboru w liście rozwijanej liryki wykonuje czynności: wysyła zapytanie 5, wyświetla komunikat w paragrafie i wysyła zapytanie 4.

    Dwa filtry na wejściu:

    • filter_input(INPUT_POST, 'poetry', FILTER_VALIDATE_INT) - liczba całkowita z listy poetry, false dla czegoś innego, null, gdy wysłano inny formularz albo żaden,
    • biała lista: isset($books['liryka'][$bookId]) - id musi być jednym z id liryki wczytanych przed chwilą z bazy. Formularz wysłany ręcznie z id dramatu albo z id 999 nie przejdzie.

    Dopiero po obu warunkach skrypt cokolwiek robi. Podgląd od tego kroku: stan po wybraniu „Na dom w Czarnolesie” i kliknięciu „Rezerwuj” (ilustracja 5).

    biblioteka.php PHP zmienione: 4 linie
    <?php
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $bookId = filter_input(INPUT_POST, 'poetry', FILTER_VALIDATE_INT);
        if (is_int($bookId) && isset($books['liryka'][$bookId])) {
        }
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 37.

  39. Krok 39: Skrypt 3: zapytanie 5 z id z formularza

    Kryteria:

    R.5.4 Przynajmniej jeden skrypt zawiera instrukcję wysyłającą do bazy danych zapytanie

    ok. 2 min
    Polecenie z arkusza

    Wysyła do bazy danych zapytanie 5, zmodyfikowane w ten sposób, że w klauzuli WHERE podana jest wartość id wysłana przez formularz.

    Zapytanie 5 przygotowane z parametrem :id (R.5.4). fetchColumn() zwraca od razu wartość pierwszej kolumny pierwszego wiersza - tytuł - bez tablicy i bez pętli.

    biblioteka.php PHP zmienione: 3 linie
    <?php
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $bookId = filter_input(INPUT_POST, 'poetry', FILTER_VALIDATE_INT);
        if (is_int($bookId) && isset($books['liryka'][$bookId])) {
            $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
            $titleQuery->execute(['id' => $bookId]);
            $reservedTitle = $titleQuery->fetchColumn();
        }
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 37.

  40. Krok 40: Skrypt 3: komunikat o rezerwacji po przekierowaniu

    Kryteria:

    R.5.6 Skrypt 3 wyświetla zwrócony zapytaniem tytuł. Tytuł jest równy wybranemu z listy rozwijanej. Jest wyświetlany w postaci: "Książka <tytuł> została zarezerwowana"

    ok. 4 min
    Polecenie z arkusza

    Wyświetla w paragrafie tekst Książka <tytuł> została zarezerwowana, gdzie pole <> zawiera wartość wybraną z bazy.

    Rezerwacja zapisuje dane, więc po niej strona stosuje wzorzec PRG (Post/Redirect/Get):

    1. sekcja i tytuł z zapytania 5 trafiają do sesji ($_SESSION['reserved'], sesję otwiera session_start()),
    2. po wszystkich operacjach na bazie header('Location: biblioteka.php', true, 303) każe przeglądarce otworzyć stronę od nowa GET-em,
    3. przy tym wejściu $reserved odbiera dane z sesji i od razu je usuwa - komunikat pokaże się raz. Odbiór stoi przed obsługą formularza, bo w żądaniu POST sesja jest jeszcze pusta.

    Bez przekierowania odświeżenie strony (F5) wysłałoby rezerwację ponownie. Paragraf składa funkcja reservationMessage($reserved, 'poetry'): zwraca komunikat tylko wtedy, gdy rezerwacja dotyczyła tej sekcji - stąd ilustracja 5 ma paragraf wyłącznie w Liryce (R.5.6). Tytuł przechodzi przez e().

    Typowe błędy

    header() po wypisaniu czegokolwiek (nawet pustej linii przed <?php) nie zadziała - „headers already sent”.

    biblioteka.php PHP zmienione: 21 linii
    <?php
    session_start();
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    $redirect = false;
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $reserved = $_SESSION['reserved'] ?? null;
        unset($_SESSION['reserved']);
    
        $bookId = filter_input(INPUT_POST, 'poetry', FILTER_VALIDATE_INT);
        if (is_int($bookId) && isset($books['liryka'][$bookId])) {
            $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
            $titleQuery->execute(['id' => $bookId]);
            $_SESSION['reserved'] = ['section' => 'poetry', 'title' => $titleQuery->fetchColumn()];
            $redirect = true;
        }
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    if ($redirect) {
        header('Location: biblioteka.php', true, 303);
        exit;
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    function reservationMessage(?array $reserved, string $section): string
    {
        if ($reserved === null || $reserved['section'] !== $section) {
            return '';
        }
        return '<p>Książka ' . e($reserved['title']) . ' została zarezerwowana</p>';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'poetry') ?>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  41. Krok 41: Skrypt 3: zapytanie 4 z id z formularza

    Kryteria: ,

    R.5.4 Przynajmniej jeden skrypt zawiera instrukcję wysyłającą do bazy danych zapytanie

    R.5.7 Skrypt 3 dla wybranej z listy książki uaktualnia jej rekord w bazie danych, wpisując do pola rezerwacja wartość 1. Należy sprawdzić w phpMyAdmin

    ok. 2 min
    Polecenie z arkusza

    Wysyła do bazy danych zmodyfikowane zapytanie 4, w ten sposób że w klauzuli WHERE podana jest wartość id wysłana przez formularz.

    Zapytanie 4 przygotowane z tym samym :id - w bazie rezerwacja wybranej książki zmienia się na 1 (R.5.7, sprawdzane w phpMyAdmin). Kolumnę dodało zapytanie 3.

    biblioteka.php PHP zmienione: 2 linie
    <?php
    session_start();
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    $redirect = false;
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $reserved = $_SESSION['reserved'] ?? null;
        unset($_SESSION['reserved']);
    
        $bookId = filter_input(INPUT_POST, 'poetry', FILTER_VALIDATE_INT);
        if (is_int($bookId) && isset($books['liryka'][$bookId])) {
            $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
            $titleQuery->execute(['id' => $bookId]);
            $_SESSION['reserved'] = ['section' => 'poetry', 'title' => $titleQuery->fetchColumn()];
            $reserveQuery = $db->prepare('UPDATE ksiazka SET rezerwacja = 1 WHERE id = :id');
            $reserveQuery->execute(['id' => $bookId]);
            $redirect = true;
        }
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    if ($redirect) {
        header('Location: biblioteka.php', true, 303);
        exit;
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    function reservationMessage(?array $reserved, string $section): string
    {
        if ($reserved === null || $reserved['section'] !== $section) {
            return '';
        }
        return '<p>Książka ' . e($reserved['title']) . ' została zarezerwowana</p>';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'poetry') ?>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 40.

  42. Krok 42: Skrypt 3 dla trzech sekcji: pętla po tablicy sekcji

    Kryteria: , ,

    R.5.6 Skrypt 3 wyświetla zwrócony zapytaniem tytuł. Tytuł jest równy wybranemu z listy rozwijanej. Jest wyświetlany w postaci: "Książka <tytuł> została zarezerwowana"

    R.5.7 Skrypt 3 dla wybranej z listy książki uaktualnia jej rekord w bazie danych, wpisując do pola rezerwacja wartość 1. Należy sprawdzić w phpMyAdmin

    R.5.8 Skrypty 2 i 3 działają we wszystkich trzech sekcjach zgadza się gatunek "Liryka", "Epika", "Dramat" z wyświetlanymi w danej liście tytułami oraz tekst pojawia się tylko pod listą w której był dokonany wybór

    ok. 3 min
    Polecenie z arkusza

    Skrypt 3 do sekcji „Epika” należy powielić i dostosować: pobiera on dane wysłane formularzem z sekcji „Epika”. Skrypt 3 do sekcji „Dramat” należy powielić i dostosować: pobiera on dane wysłane formularzem z sekcji „Dramat”. Należy zwrócić uwagę na to, że wyświetlany w wyniku wykonania formularza paragraf powinien pojawić się tylko w odpowiedniej sekcji.

    Ta sama tablica $sections co w skrypcie 2: pętla foreach ($sections as $field => $genre) sprawdza po kolei listy poetry, epic i drama. Wysłany jest zawsze tylko jeden formularz, więc dane znajdzie co najwyżej jeden obieg - i to jego sekcja ($field) trafia do sesji. Biała lista sprawdza id w książkach tego gatunku.

    W HTML-u pod formularzem Epiki i Dramatu stoi to samo wywołanie z inną nazwą sekcji. Komunikat pojawia się tylko tam, gdzie kliknięto „Rezerwuj” (R.5.8).

    biblioteka.php PHP zmienione: 12 linii
    <?php
    session_start();
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    $redirect = false;
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $reserved = $_SESSION['reserved'] ?? null;
        unset($_SESSION['reserved']);
    
        foreach ($sections as $field => $genre) {
            $bookId = filter_input(INPUT_POST, $field, FILTER_VALIDATE_INT);
            if (is_int($bookId) && isset($books[$genre][$bookId])) {
                $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
                $titleQuery->execute(['id' => $bookId]);
                $_SESSION['reserved'] = ['section' => $field, 'title' => $titleQuery->fetchColumn()];
                $reserveQuery = $db->prepare('UPDATE ksiazka SET rezerwacja = 1 WHERE id = :id');
                $reserveQuery->execute(['id' => $bookId]);
                $redirect = true;
            }
        }
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    if ($redirect) {
        header('Location: biblioteka.php', true, 303);
        exit;
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    function reservationMessage(?array $reserved, string $section): string
    {
        if ($reserved === null || $reserved['section'] !== $section) {
            return '';
        }
        return '<p>Książka ' . e($reserved['title']) . ' została zarezerwowana</p>';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'poetry') ?>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'epic') ?>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'drama') ?>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  43. Krok 43: Skrypt 4: zapytanie 2 do bazy

    Kryteria:

    R.5.4 Przynajmniej jeden skrypt zawiera instrukcję wysyłającą do bazy danych zapytanie

    ok. 2 min
    Polecenie z arkusza

    Skrypt 4: wysyła do bazy danych zapytanie 2.

    Zapytanie 2 bez danych od użytkownika - query i fetchAll() do $overdue (R.5.4). Długie zapytanie można złamać na dwie linie w napisie; SQL nie zwraca uwagi na znaki nowej linii.

    biblioteka.php PHP zmienione: 5 linii
    <?php
    session_start();
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    $redirect = false;
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $reserved = $_SESSION['reserved'] ?? null;
        unset($_SESSION['reserved']);
    
        foreach ($sections as $field => $genre) {
            $bookId = filter_input(INPUT_POST, $field, FILTER_VALIDATE_INT);
            if (is_int($bookId) && isset($books[$genre][$bookId])) {
                $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
                $titleQuery->execute(['id' => $bookId]);
                $_SESSION['reserved'] = ['section' => $field, 'title' => $titleQuery->fetchColumn()];
                $reserveQuery = $db->prepare('UPDATE ksiazka SET rezerwacja = 1 WHERE id = :id');
                $reserveQuery->execute(['id' => $bookId]);
                $redirect = true;
            }
        }
    
        $overdue = $db->query(
            'SELECT k.tytul, w.id_cz, w.data_odd FROM ksiazka AS k JOIN wypozyczenia AS w ON w.id_ks = k.id
             ORDER BY w.data_odd LIMIT 15'
        )->fetchAll();
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    if ($redirect) {
        header('Location: biblioteka.php', true, 303);
        exit;
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    function reservationMessage(?array $reserved, string $section): string
    {
        if ($reserved === null || $reserved['section'] !== $section) {
            return '';
        }
        return '<p>Książka ' . e($reserved['title']) . ' została zarezerwowana</p>';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'poetry') ?>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'epic') ?>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'drama') ?>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 42.

  44. Krok 44: Skrypt 4: element listy z trzema wartościami

    Kryteria: ,

    R.2.9 W czwartym bloku sekcji umieszczona jedna lista punktowana z elementami (w HTML lub skrypcie) , zastosowane znaczniki <ul> i <li>

    R.5.9 Skrypt 4 wyświetla w sekcji czwartej zwrócone zapytaniem kolejne rekordy w elementach listy: dane tytuł, id czytelnika i datę (nie wszystkie muszą być widoczne ze względu na właściwość overflow)

    ok. 2 min
    Polecenie z arkusza

    Każdy zwrócony zapytaniem wiersz wyświetla w elemencie listy kolejno: tytuł, id czytelnika i datę oddania oddzielone od siebie spacją.

    Jeden <li> na wypożyczenie, wartości po nazwach kolumn rozdzielone spacjami (R.2.9, R.5.9). Dolne pozycje mogą być obcięte przez overflow: hidden - klucz to dopuszcza.

    biblioteka.php PHP zmienione: 3 linie
    <?php
    session_start();
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    $redirect = false;
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $reserved = $_SESSION['reserved'] ?? null;
        unset($_SESSION['reserved']);
    
        foreach ($sections as $field => $genre) {
            $bookId = filter_input(INPUT_POST, $field, FILTER_VALIDATE_INT);
            if (is_int($bookId) && isset($books[$genre][$bookId])) {
                $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
                $titleQuery->execute(['id' => $bookId]);
                $_SESSION['reserved'] = ['section' => $field, 'title' => $titleQuery->fetchColumn()];
                $reserveQuery = $db->prepare('UPDATE ksiazka SET rezerwacja = 1 WHERE id = :id');
                $reserveQuery->execute(['id' => $bookId]);
                $redirect = true;
            }
        }
    
        $overdue = $db->query(
            'SELECT k.tytul, w.id_cz, w.data_odd FROM ksiazka AS k JOIN wypozyczenia AS w ON w.id_ks = k.id
             ORDER BY w.data_odd LIMIT 15'
        )->fetchAll();
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    if ($redirect) {
        header('Location: biblioteka.php', true, 303);
        exit;
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    function reservationMessage(?array $reserved, string $section): string
    {
        if ($reserved === null || $reserved['section'] !== $section) {
            return '';
        }
        return '<p>Książka ' . e($reserved['title']) . ' została zarezerwowana</p>';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'poetry') ?>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'epic') ?>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'drama') ?>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
    <?php foreach ($overdue as $loan): ?>
                <li><?= e($loan['tytul']) ?> <?= e($loan['id_cz']) ?> <?= e($loan['data_odd']) ?></li>
    <?php endforeach; ?>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (biblioteka.php)
  45. Krok 45: Zamknięcie połączenia z bazą

    Kryteria:

    R.5.2 Ostatnią operacją na bazie jest jej zamknięcie

    ok. 1 min
    Polecenie z arkusza

    Na końcu zamykane jest połączenie z serwerem.

    $db = null; na końcu bloku try zamyka połączenie PDO (R.5.2) - po wszystkich zapytaniach i przed przekierowaniem. Szablon korzysta już tylko z gotowych tablic.

    Typowe błędy

    $db->close() nie istnieje w PDO.

    biblioteka.php PHP zmienione: 2 linie
    <?php
    session_start();
    $config = require 'config.php';
    $sections = ['poetry' => 'liryka', 'epic' => 'epika', 'drama' => 'dramat'];
    $redirect = false;
    
    try {
        $db = new PDO($config['dsn'], $config['user'], $config['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        $booksQuery = $db->prepare('SELECT id, tytul FROM ksiazka WHERE gatunek = :genre');
        $books = [];
        foreach ($sections as $genre) {
            $booksQuery->execute(['genre' => $genre]);
            $books[$genre] = $booksQuery->fetchAll(PDO::FETCH_KEY_PAIR);
        }
    
        $reserved = $_SESSION['reserved'] ?? null;
        unset($_SESSION['reserved']);
    
        foreach ($sections as $field => $genre) {
            $bookId = filter_input(INPUT_POST, $field, FILTER_VALIDATE_INT);
            if (is_int($bookId) && isset($books[$genre][$bookId])) {
                $titleQuery = $db->prepare('SELECT tytul FROM ksiazka WHERE id = :id');
                $titleQuery->execute(['id' => $bookId]);
                $_SESSION['reserved'] = ['section' => $field, 'title' => $titleQuery->fetchColumn()];
                $reserveQuery = $db->prepare('UPDATE ksiazka SET rezerwacja = 1 WHERE id = :id');
                $reserveQuery->execute(['id' => $bookId]);
                $redirect = true;
            }
        }
    
        $overdue = $db->query(
            'SELECT k.tytul, w.id_cz, w.data_odd FROM ksiazka AS k JOIN wypozyczenia AS w ON w.id_ks = k.id
             ORDER BY w.data_odd LIMIT 15'
        )->fetchAll();
    
        $db = null;
    } catch (PDOException $error) {
        http_response_code(500);
        exit('Baza danych jest chwilowo niedostępna. Spróbuj ponownie później.');
    }
    
    if ($redirect) {
        header('Location: biblioteka.php', true, 303);
        exit;
    }
    
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    function reservationMessage(?array $reserved, string $section): string
    {
        if ($reserved === null || $reserved['section'] !== $section) {
            return '';
        }
        return '<p>Książka ' . e($reserved['title']) . ' została zarezerwowana</p>';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Biblioteka miejska</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
    <?php for ($i = 0; $i < 20; $i++): ?>
            <img src="obraz.png" alt="">
    <?php endfor; ?>
        </header>
        <section>
            <h2>Liryka</h2>
            <form method="post">
                <select name="poetry" aria-label="Książka">
    <?php foreach ($books['liryka'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'poetry') ?>
        </section>
        <section>
            <h2>Epika</h2>
            <form method="post">
                <select name="epic" aria-label="Książka">
    <?php foreach ($books['epika'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'epic') ?>
        </section>
        <section>
            <h2>Dramat</h2>
            <form method="post">
                <select name="drama" aria-label="Książka">
    <?php foreach ($books['dramat'] as $id => $title): ?>
                    <option value="<?= $id ?>"><?= e($title) ?></option>
    <?php endforeach; ?>
                </select>
                <button type="submit">Rezerwuj</button>
            </form>
            <?= reservationMessage($reserved, 'drama') ?>
        </section>
        <section>
            <h2>Zaległe książki</h2>
            <ul>
    <?php foreach ($overdue as $loan): ?>
                <li><?= e($loan['tytul']) ?> <?= e($loan['id_cz']) ?> <?= e($loan['data_odd']) ?></li>
    <?php endforeach; ?>
            </ul>
        </section>
        <footer>
            <p><strong>Autor: Bartosz Bryniarski</strong></p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 44.

  46. Etap 5: Porównanie z wersją podstawową

    Krok 46 · po tym etapie masz 42 z 42 kryteriów (pkt)

    Krok 46: Co jest inaczej i którą wersję wybrać na egzaminie

    ok. 2 min

    Obie wersje dają komplet 42 punktów. Różnice:

    wersja podstawowawersja nowoczesna
    układfloat + clear w @media (min-width: 801px)siatka na <body>, „mobile first”
    skrypty 2 i 3trzy kopie, na końcu funkcjetablica $sections i pętla od początku
    bazamysqli_*, id sklejone z SQLPDO, zapytania przygotowane, config.php
    dane z formularzaisset($_POST[...])filter_input i biała lista id z bazy
    po rezerwacjikomunikat od razuPRG: przekierowanie, komunikat z sesji
    grafiki w nagłówkuecho w pętli, alt z tekstemszablon for … endfor, alt="" dla dekoracji

    Na egzaminie: arkusz wprost każe skopiować skrypty - kopiowanie jest szybkie i trudno się przy nim pomylić, a klucz ocenia działanie, więc obie drogi są bezpieczne. Pętla po tablicy opłaca się, gdy sekcji jest więcej albo zapytanie trzeba poprawić. Formularz z prawdziwą rezerwacją dostałby jeszcze token CSRF (ochrona przed wysłaniem formularza z cudzej strony) - klucz tego nie wymaga.

    Podgląd strony bez zmian - taki jak po kroku 44.

  47. Etap 6: Oddanie pracy

    Krok 47 · po tym etapie masz 42 z 42 kryteriów (pkt)

    Krok 47: 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. Nagraj płytę z rezultatami pracy.

    W folderze z numerem zdającego: biblioteka.php, config.php, import.png, kw1.jpg … kw5.jpg, kwerendy.txt, obraz.png, przeglądarka.txt, styl.css. Płytę sprawdź po nagraniu.

    przeglądarka.txt Tekst nowy plik
    Google Chrome

    Podgląd strony bez zmian - taki jak po kroku 44.

Podgląd

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: . 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.