Przejdź do głównej treści
INF.03-10-25.06-SGCzerwiec 2025 · 49 kroków · ok. 85 min · 39 kryteriów z klucza

Rozwiązanie krok po kroku: Portal szkoleniowy z bazą danych (Nowoczesny kod (grid, PDO))

To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza szkolenia, cztery zapytania, zdjęcie przeskalowane do 150 px, strona index.php z tabelą kursów i formularzem zapisu, style w styl.css - i tak samo spełnia każde kryterium klucza. Inna jest droga:

  • układ na siatce CSS (display: grid i grid-template-columns) zamiast flex, kolory z arkusza w zmiennych CSS, formularz ułożony siatką bez znaczników <br>,
  • PHP na PDO zamiast funkcji mysqli_*: zapytanie przygotowane z parametrami nazwanymi dla INSERT, wyjątki zamiast sprawdzania błędów ręcznie, dane logowania w osobnym pliku config.php,
  • logika przed HTML-em: wszystkie operacje na bazie wykonują się na początku pliku, a niżej jest już tylko szablon strony, który wypisuje gotowe dane przez funkcję e() (escapowanie),
  • PRG po zapisie: po dodaniu uczestnika strona przekierowuje sama na siebie, więc odświeżenie nie doda go drugi raz.

Wersja podstawowa (zakładka obok) pisze kod najprostszymi funkcjami z tabeli 1 arkusza i dopiero na końcu go poprawia; tu kod jest od razu taki, jak w prawdziwym projekcie. Na końcu jest krótkie porównanie obu wersji i podpowiedź, którą wybrać na egzaminie.

Baza i zdjęcia pochodzą z prawdziwego archiwum pliki9 (załącznik na stronie arkusza, hasło w treści zadania), a podgląd każdego kroku z PHP to wynik prawdziwego uruchomienia strony na serwerze z tą bazą.

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

  1. Etap 1: Baza danych

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

    Krok 1: Baza danych szkolenia

    Kryteria:

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

    ok. 2 min
    Polecenie z arkusza

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

    W phpMyAdmin (http://localhost/phpmyadmin) kliknij Nowa, wpisz nazwę szkolenia i wybierz metodę porównywania utf8_polish_ci. To samo w SQL:

    CREATE DATABASE szkolenia CHARACTER SET utf8 COLLATE utf8_polish_ci;
    

    O kolejności sortowania decyduje jednak porównywanie zapisane w definicji tabel z szkolimy.sql (utf8_polish_ci, np. „ł” po „l”) - ustawienie bazy dziedziczą tylko tabele zakładane później bez własnego ustawienia. Baza nazywa się szkolenia, plik do importu szkolimy.sql - to różne słowa.

    Typowe błędy

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

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

    Kryteria:

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

    ok. 2 min
    Polecenie z arkusza

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

    Zaznacz bazę szkolenia w lewym panelu, karta Import, plik szkolimy.sql, Wykonaj. Powstają trzy tabele z ilustracji 1: kursy (8 kursów, kod 1-8 to zarazem nazwa pliku zdjęcia), uczestnicy (29 osób, klucz id_uczestnika z AUTO_INCREMENT) i tabela łącząca kursy_uczestnicy (46 zapisów).

    Typowe błędy

    Import bez zaznaczonej bazy szkolenia tworzy tabele w innej bazie - strona ich nie znajdzie.

  3. Krok 3: Zrzut ekranu import.png

    Kryteria:

    R.1.1 Wykonany import tabel do bazy danych szkolenia , 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ń. Na zrzucie powinny być widoczne elementy wskazujące na poprawnie wykonany import tabel.

    Cały ekran (Print Screen), z komunikatem o udanym imporcie albo listą trzech tabel, zapis jako import.png. W tym arkuszu zrzut importu jest w PNG, a zrzuty zapytań w JPEG.

    Typowe błędy

    Wykadrowany zrzut (bez paska zadań) nie jest oceniany.

  4. Krok 4: Zapytanie 1: kursy od najtańszego

    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 kod, nazwę i cenę kursów posortowane rosnąco według ceny. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT kod, nazwa, cena FROM kursy ORDER BY cena; (ASC opcjonalne) oraz wynik działania zapytania udokumentowany zrzutem, na którym widocznych jest dokładnie 8 rekordów dla wszystkich kursów, pierwszy rekord 5 gastronomia 1000, ostatni 7 spawanie 3200

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 1: wybierające jedynie kod, nazwę i cenę kursów posortowane rosnąco według ceny.

    Każda klauzula w osobnej linii - tak zapytanie czyta się od góry do dołu: co (SELECT kod, nazwa, cena), skąd (FROM kursy), w jakiej kolejności (ORDER BY cena ASC). ASC jest domyślne, ale wpisane wprost mówi czytelnikowi, że kierunek jest zamierzony.

    Wynik: 8 kursów, od 5 gastronomia 1000 do 7 spawanie 3200 (klucz R.1.3). Zapytanie do kwerendy.txt, zrzut kw1.jpg.

    Typowe błędy

    SELECT * łamie słowo „jedynie” - zwraca też max_liczba_miejsc.

    kwerendy.txt Tekst nowy plik
    -- Zapytanie 1
    SELECT kod, nazwa, cena
    FROM kursy
    ORDER BY cena ASC;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 4
  5. Krok 5: Zapytanie 2: nazwy wszystkich kursów

    Kryteria:

    R.1.4 Wykonane zapytanie 2 wybierające jedynie nazwy wszystkich kursów. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nazwa FROM kursy; oraz wynik działania zapytania udokumentowany zrzutem, na którym widocznych jest dokładnie 8 rekordów: pierwszy jest język angielski, ostatni rysunek techniczny

    ok. 1 min
    Polecenie z arkusza

    Zapytanie 2: wybierające jedynie nazwy wszystkich kursów.

    Jedna kolumna, bez warunku i bez sortowania - 8 nazw w kolejności z tabeli, od języka angielskiego do rysunku technicznego (klucz R.1.4). Zrzut kw2.jpg. W tej samej kolejności kursy pojawią się w liście rozwijanej (ilustracja 4).

    kwerendy.txt Tekst zmienione: 4 linie
    -- Zapytanie 1
    SELECT kod, nazwa, cena
    FROM kursy
    ORDER BY cena ASC;
    
    -- Zapytanie 2
    SELECT nazwa
    FROM kursy;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 5
  6. Krok 6: Zapytanie 3: nowy uczestnik Tadeusz Wysocki

    Kryteria:

    R.1.5 Wykonane zapytanie 3 wstawiające rekord do tabeli uczestnicy o danych: Tadeusz Wysocki, wiek 36 lat. Klucz główny nadawany automatycznie. W pliku z kwerendami lub na zrzucie istnieje zapis np.: INSERT INTO `uczestnicy`(`imie`, `nazwisko`, `wiek`) VALUES ('Tadeusz','Wysocki','36'); oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczny jest komunikat o poprawnym wstawieniu wiersza lub tabela ze wstawionym wierszem

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 3: wstawiające rekord do tabeli uczestnicy o danych: Tadeusz Wysocki, wiek 36 lat. Klucz główny nadawany automatycznie.

    Lista kolumn podana jawnie (imie, nazwisko, wiek), bez id_uczestnika: kolumna ma AUTO_INCREMENT, więc baza nada numer 30 sama. Jawna lista kolumn ma też drugą zaletę - zapytanie działa dalej, gdy ktoś dopisze do tabeli nową kolumnę.

    Komunikat „1 wiersz wstawiony” to zrzut kw3.jpg (klucz R.1.5).

    Typowe błędy

    Wpisanie własnego id_uczestnika nie spełnia „klucz główny nadawany automatycznie”.

    kwerendy.txt Tekst zmienione: 4 linie
    -- Zapytanie 1
    SELECT kod, nazwa, cena
    FROM kursy
    ORDER BY cena ASC;
    
    -- Zapytanie 2
    SELECT nazwa
    FROM kursy;
    
    -- Zapytanie 3
    INSERT INTO uczestnicy (imie, nazwisko, wiek)
    VALUES ('Tadeusz', 'Wysocki', 36);
    Podgląd strony po tym kroku
    Podgląd strony po kroku 6
  7. Krok 7: Zapytanie 4: liczba zapisanych na każdy kurs

    Kryteria:

    R.1.6 Wykonane zapytanie 4 tworzące zestawienie, na którym widoczne są nazwy wszystkich kursów oraz liczby osób zapisanych na te kursy. Nazwa kolumny z liczbą osób (alias): „Zapisanych”. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT COUNT(kod_kursu) AS 'Zapisanych', nazwa FROM kursy JOIN kursy_uczestnicy ON kod = kod_kursu GROUP BY nazwa; (AS opcjonalne) oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczne są dane: 6 florystyka, 12 gastronomia, 7 informatyka, 2 język angielski, 6 język francuski, 6 język niemiecki, 4 rysunek techniczny, 3 spawanie

    ok. 4 min
    Polecenie z arkusza

    Zapytanie 4: tworzące zestawienie, na którym widoczne są nazwy wszystkich kursów oraz liczby osób zapisanych na te kursy. Nazwa kolumny z liczbą osób (alias): „Zapisanych”.

    Złączenie z aliasami tabel (kursy AS k, kursy_uczestnicy AS ku) - każda kolumna mówi, z której tabeli pochodzi, więc zapytanie nie zależy od tego, że nazwy kolumn przypadkiem się nie powtarzają:

    • JOIN … ON ku.kod_kursu = k.kod - do każdego zapisu dołączony kurs,
    • COUNT(ku.id_uczestnika) AS Zapisanych - liczba osób w grupie, z aliasem z arkusza,
    • GROUP BY k.kod, k.nazwa - grupa = jeden kurs (po kluczu, więc dwa kursy o tej samej nazwie by się nie zlały),
    • ORDER BY k.nazwa - kolejność alfabetyczna podana wprost; samo GROUP BY nie gwarantuje kolejności.

    Wynik: florystyka 6, gastronomia 12, informatyka 7, język angielski 2, francuski 6, niemiecki 6, rysunek techniczny 4, spawanie 3 - jak w kluczu R.1.6. Zrzut kw4.jpg.

    Typowe błędy

    COUNT bez GROUP BY daje jeden wiersz z liczbą wszystkich zapisów (46).

    kwerendy.txt Tekst zmienione: 7 linii
    -- Zapytanie 1
    SELECT kod, nazwa, cena
    FROM kursy
    ORDER BY cena ASC;
    
    -- Zapytanie 2
    SELECT nazwa
    FROM kursy;
    
    -- Zapytanie 3
    INSERT INTO uczestnicy (imie, nazwisko, wiek)
    VALUES ('Tadeusz', 'Wysocki', 36);
    
    -- Zapytanie 4
    SELECT k.nazwa, COUNT(ku.id_uczestnika) AS Zapisanych
    FROM kursy AS k
    JOIN kursy_uczestnicy AS ku ON ku.kod_kursu = k.kod
    GROUP BY k.kod, k.nazwa
    ORDER BY k.nazwa;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 7
  8. Etap 2: Grafika i strona HTML

    Kroki 8-24 · po tym etapie masz 17 z 39 kryteriów (pkt)

    Krok 8: Grafika 1.jpg: szerokość 150 px

    Kryteria:

    R.2.1 Grafika 1.jpg przeskalowana z zachowaniem proporcji do szerokości 150 px, odpowiadająca temu wysokość wynosi 100 px (± 5 px)

    ok. 3 min
    Polecenie z arkusza

    Cechy grafiki 1.jpg: przeskalowana z zachowaniem proporcji do szerokości 150 px.

    1.jpg ma 450 × 300 px; Obraz → Skaluj obraz w GIMP-ie, szerokość 150 px przy zamkniętym łańcuchu proporcji - wysokość wyjdzie 100 px (klucz: 100 ±5). Pozostałe zdjęcia już mają 150 × 100. Skalowanie w edytorze, a nie atrybutem width, zmniejsza też plik - przeglądarka nie pobiera trzy razy większego obrazu.

    Podgląd strony po tym kroku
    Podgląd strony po kroku 8
  9. Krok 9: Plik index.php w HTML5

    Kryteria:

    R.2.2 W pliku index.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 index.php. Zapisana w języku HTML5.

    Plik index.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.

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

    Kryteria:

    R.2.2 W pliku index.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.

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

    Kryteria:

    R.2.2 W pliku index.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.

    index.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 (index.php)
  12. Krok 12: Tytuł strony

    Kryteria:

    R.2.3 Stronie nadany tytuł "Szkolenia i kursy"

    ok. 1 min
    Polecenie z arkusza

    Tytuł strony „Szkolenia i kursy”.

    <title>Szkolenia i kursy</title> - napis na karcie przeglądarki (klucz R.2.3). 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.

    index.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>Szkolenia i kursy</title>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  13. Krok 13: 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 (R.3.2). 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.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  14. Krok 14: Bloki semantyczne

    Kryteria:

    R.2.4 W sekcji body zdefiniowany jest podział na bloki za pomocą semantycznych znaczników HTML 5: header, main->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. Zawartość bloku głównego: sekcja lewa i sekcja prawa.

    Ilustracja 3 przekłada się na znaczniki z klucza R.2.4: <header>, <main> z dwiema <section>, <footer>. Każdy znacznik mówi, czym jest blok - czytnik ekranu pozwala przeskoczyć prosto do treści głównej, a wyszukiwarka wie, co jest nawigacyjnym dodatkiem. Sekcje dostają identyfikatory left i right, bo arkusz formatuje każdą inaczej.

    Typowe błędy

    <div> w miejscu <section> nie jest znacznikiem semantycznym - klucz wymaga main → section + section.

    index.php PHP zmienione: 10 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
        </header>
        <main>
            <section id="left">
            </section>
            <section id="right">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  15. Krok 15: Blok nagłówkowy: nagłówek pierwszego stopnia

    Kryteria:

    R.2.5 Umieszczony w bloku nagłówkowym <h1>: "SZKOLENIA”, w sekcji prawej <h2>: "Zapisy na kursy" oraz paragraf z numerem zdającego w stopce

    ok. 1 min
    Polecenie z arkusza

    Zawartość bloku nagłówkowego: nagłówek pierwszego stopnia o treści: „SZKOLENIA”.

    <h1>SZKOLENIA</h1> w <header>. Rozstrzelenie liter z ilustracji 2 zrobi CSS (letter-spacing) - spacje między literami w HTML czytnik ekranu przeczytałby jako osobne litery.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
            </section>
            <section id="right">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  16. Krok 16: Sekcja lewa: tabela z wierszem nagłówkowym

    Kryteria: ,

    R.2.7 W sekcji lewej umieszczona tabela o 3 kolumnach (w skrypcie lub HTML) , zastosowane znaczniki <table>, <tr>, <td>

    R.2.8 Pierwszy wiersz tabeli zawiera komórki nagłówkowe, zastosowane znaczniki <tr>, <th>

    ok. 2 min
    Polecenie z arkusza

    Zawartość sekcji lewej: tabela o trzech kolumnach. Pierwszy wiersz zawiera komórki nagłówkowe: Kurs, Nazwa, Cena. Tabela wypełniona jest przez skrypt 1.

    Tabela podzielona na <thead> (wiersz z komórkami nagłówkowymi <th> - klucz R.2.8) i <tbody> (wiersze danych, na razie puste - wypełni je skrypt 1). Podział nie zmienia wyglądu, ale mówi przeglądarce, który wiersz jest nagłówkiem: przy wydruku długiej tabeli <thead> powtarza się na każdej stronie, a w CSS można formatować osobno nagłówek i dane.

    index.php PHP zmienione: 11 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  17. Krok 17: Sekcja prawa: nagłówek „Zapisy na kursy”

    Kryteria:

    R.2.5 Umieszczony w bloku nagłówkowym <h1>: "SZKOLENIA”, w sekcji prawej <h2>: "Zapisy na kursy" oraz paragraf z numerem zdającego w stopce

    ok. 1 min
    Polecenie z arkusza

    Zawartość sekcji prawej: nagłówek drugiego stopnia o treści: „Zapisy na kursy”.

    <h2> - drugi poziom pod <h1> z nagłówka strony; poziomy nagłówków tworzą spis treści strony, więc nie przeskakuje się z <h1> od razu do <h3>.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  18. Krok 18: Sekcja prawa: formularz

    Kryteria: ,

    R.2.9 W sekcji prawej umieszczony formularz z 4 etykietami, 3 polami edycyjnymi, listą rozwijaną i przyciskiem. Przynajmniej jedna etykieta powiązana jest z kontrolką

    R.3.3 Formularz wysyła dane metodą POST do tego samego pliku (poprawne również bez atrybutu action)

    ok. 1 min
    Polecenie z arkusza

    Formularz zgodny z ilustracją 2, wysyłający dane do tego samego pliku metodą bezpieczną z polami: etykiety, pola edycyjne, lista rozwijana i przycisk.

    method="post" - dane w treści żądania, nie w adresie. Bez atrybutu action formularz wysyła się na adres bieżącej strony (R.3.3). Etykiety staną nad polami bez znaczników <br> - ułoży je siatka w CSS.

    Typowe błędy

    Brak method to GET - dane trafią do adresu, a skrypt czytający $_POST nic nie dostanie.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  19. Krok 19: Formularz: etykieta „Imię” i pole

    Kryteria:

    R.2.9 W sekcji prawej umieszczony formularz z 4 etykietami, 3 polami edycyjnymi, listą rozwijaną i przyciskiem. Przynajmniej jedna etykieta powiązana jest z kontrolką

    ok. 1 min
    Polecenie z arkusza

    Etykieta dla pierwszego pola edycyjnego: „Imię”. Pole edycyjne.

    <label for="name"> wskazuje pole o id="name" - kliknięcie etykiety przenosi kursor do pola, a czytnik ekranu odczytuje etykietę przy wejściu w pole (R.2.9). name="name" to klucz w $_POST. autocomplete="given-name" podpowiada przeglądarce, że to imię - wypełni je z zapamiętanych danych.

    Typowe błędy

    Pole bez name nie jest wysyłane - skrypt nie dostanie imienia.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  20. Krok 20: Formularz: etykieta „Nazwisko” i pole

    Kryteria:

    R.2.9 W sekcji prawej umieszczony formularz z 4 etykietami, 3 polami edycyjnymi, listą rozwijaną i przyciskiem. Przynajmniej jedna etykieta powiązana jest z kontrolką

    ok. 1 min
    Polecenie z arkusza

    Etykieta dla drugiego pola edycyjnego: „Nazwisko”. Pole edycyjne.

    Ten sam wzorzec: etykieta z for="surname", pole z id i name równymi surname, podpowiedź family-name.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  21. Krok 21: Formularz: etykieta „Wiek” i pole liczbowe

    Kryteria: ,

    R.2.9 W sekcji prawej umieszczony formularz z 4 etykietami, 3 polami edycyjnymi, listą rozwijaną i przyciskiem. Przynajmniej jedna etykieta powiązana jest z kontrolką

    R.3.4 Pole edycyjne do wpisania wieku jest typu number oraz przycisk realizuje funkcję wysłania danych z formularza (input lub button z typem submit lub button bez typu)

    ok. 1 min
    Polecenie z arkusza

    Etykieta dla trzeciego pola edycyjnego: „Wiek”. Pole edycyjne typu numerycznego.

    type="number" (R.3.4) z zakresem min="1" max="120" - przeglądarka nie przyjmie liter ani wieku spoza zakresu. To tylko wygoda dla użytkownika: ten sam zakres sprawdzi PHP, bo żądanie można wysłać z pominięciem formularza.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  22. Krok 22: Formularz: etykieta „Rodzaj kursu” i lista rozwijana

    Kryteria:

    R.2.9 W sekcji prawej umieszczony formularz z 4 etykietami, 3 polami edycyjnymi, listą rozwijaną i przyciskiem. Przynajmniej jedna etykieta powiązana jest z kontrolką

    ok. 1 min
    Polecenie z arkusza

    Etykieta dla listy rozwijanej: „Rodzaj kursu”. Lista rozwijana wypełniona przez skrypt 2.

    Czwarta etykieta i <select> bez opcji - wstawi je skrypt 2. Zapytanie 3 zapisuje tylko imię, nazwisko i wiek, więc wybranego kursu skrypt 3 nie czyta.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  23. Krok 23: Formularz: przycisk „Dodaj dane”

    Kryteria: ,

    R.2.9 W sekcji prawej umieszczony formularz z 4 etykietami, 3 polami edycyjnymi, listą rozwijaną i przyciskiem. Przynajmniej jedna etykieta powiązana jest z kontrolką

    R.3.4 Pole edycyjne do wpisania wieku jest typu number oraz 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 „Dodaj dane” wysyłający dane z formularza do skryptu 3.

    <button type="submit"> - typ wpisany jawnie, bo <button> wewnątrz formularza bez typu też wysyła, ale czytający kod musiałby to wiedzieć (R.3.4). Formularz ma komplet z R.2.9: 4 etykiety, 3 pola, listę i przycisk.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  24. Krok 24: Stopka z numerem zdającego

    Kryteria:

    R.2.5 Umieszczony w bloku nagłówkowym <h1>: "SZKOLENIA”, w sekcji prawej <h2>: "Zapisy na kursy" oraz paragraf z numerem zdającego w stopce

    ok. 1 min
    Polecenie z arkusza

    Zawartość stopki: paragraf o treści „Stronę wykonał: ”, dalej wstawiony numer zdającego.

    Paragraf w <footer>. Tu stoi nazwisko autora rozwiązania - na egzaminie po dwukropku wpisujesz swój numer zdającego (PESEL), bo tego sprawdza klucz R.2.5.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  25. Etap 3: Styl CSS

    Kroki 25-33 · po tym etapie masz 28 z 39 kryteriów (pkt)

    Krok 25: Plik styl.css: kolory z arkusza w zmiennych

    ok. 2 min
    Polecenie z arkusza

    Styl CSS zdefiniowany jest w całości w zewnętrznym pliku o nazwie styl.css.

    Na początku pliku trzy kolory z arkusza jako zmienne CSS (custom properties) w :root. Bordowy #880E4F występuje w arkuszu w trzech blokach - zmienna --accent trzyma go w jednym miejscu, a jego zmiana to jedna linia. Odwołanie to var(--accent).

    Typowe błędy

    Egzaminator sprawdza style w inspektorze przeglądarki, który pokazuje wartość obliczoną (rgb(136, 14, 79) = #880E4F). Jeśli wolisz nie ryzykować sporu przy czytaniu samego pliku CSS, wpisz kolory wprost - wersja podstawowa tak robi.

    styl.css CSS nowy plik
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    Podgląd strony po tym kroku (index.php)
  26. Krok 26: Ciało strony

    Kryteria:

    R.4.1 Ustawiony dla selektora body: krój czcionki "Century Gothic", sans-serif (tylko taka kolejność) , wyrównanie tekstu do środka, kolor tła #FCE4EC

    ok. 1 min
    Polecenie z arkusza

    Dla selektora ciała strony: krój czcionki Century Gothic, w przypadku braku sans-serif, wyrównanie tekstu do środka, kolor tła #FCE4EC.

    Selektor body - uwaga arkusza wymaga formatowania ciała strony wyłącznie selektorem znacznika. Krój dwuwyrazowy w cudzysłowie, po nim rodzina zapasowa sans-serif (R.4.1 - tylko ta kolejność). text-align: center dziedziczą wszystkie bloki.

    styl.css CSS zmienione: 6 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    Podgląd strony po tym kroku (index.php)
  27. Krok 27: Nagłówek i stopka

    Kryteria: , , ,

    R.4.2 Ustawiony kolor tła dla bloku nagłówkowego, prawej sekcji i stopki #880E4F

    R.4.3 Ustawiony biały kolor czcionki dla bloku nagłówkowego, prawej sekcji i stopki

    R.4.6 Ustawione marginesy wewnętrzne (padding) dla bloku nagłówkowego i stopki 10px oraz ustawione marginesy zewnętrzne (margin): dla tabeli auto, dla prawej sekcji 35px i dla przynajmniej jednej kontrolki 5px

    R.4.7 Ustawiona dla bloku nagłówkowego i stopki odległość między literami 20px

    ok. 2 min
    Polecenie z arkusza

    Dla bloku nagłówkowego i stopki: kolor tła #880E4F, biały kolor czcionki, marginesy wewnętrzne 10 px, odległość między literami 20 px.

    Jedna reguła dla dwóch selektorów (każdy w osobnej linii - łatwiej dopisać trzeci). letter-spacing: 20px rozstrzela litery jak na ilustracji 2 (R.4.7).

    Typowe błędy

    word-spacing rozsuwa wyrazy, nie litery.

    styl.css CSS zmienione: 8 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    Podgląd strony po tym kroku (index.php)
  28. Krok 28: Układ bloków na siatce

    Kryteria:

    R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z ilustracją 3 w arkuszu egzaminacyjnym (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)

    ok. 2 min
    Polecenie z arkusza

    Podział strony na bloki zrealizowany za pomocą semantycznych znaczników bloków języka HTML5 tak, aby po uruchomieniu w przeglądarce układ bloków na stronie był zgodny z ilustracją 3.

    <main> staje się siatką o dwóch kolumnach: grid-template-columns: 50% 40% - pierwsza kolumna dla sekcji lewej, druga dla prawej. Szerokości sekcji z arkusza są więc zapisane w jednym miejscu, przy układzie, a nie w każdej sekcji osobno - klucz R.4.4 wymienia grid-template-columns obok width. Stopka jest poza <main>, więc zostaje pod spodem bez clear (R.3.1).

    Różnica wobec flex z wersji podstawowej: w siatce margines zewnętrzny sekcji mieści się w jej kolumnie, więc prawa sekcja z marginesem 35 px będzie o 70 px węższa niż kolumna. Kolumna nadal ma 40%, a układ zgadza się z ilustracją 3.

    Typowe błędy

    width: 50% w sekcji umieszczonej w kolumnie siatki liczy się od kolumny, nie od strony - w siatce szerokości podaje się w grid-template-columns, nie w obu miejscach naraz.

    styl.css CSS zmienione: 5 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    
    main {
        display: grid;
        grid-template-columns: 50% 40%;
    }
    Podgląd strony po tym kroku (index.php)
  29. Krok 29: Sekcja lewa: wysokość i paski przewijania

    Kryteria:

    R.4.9 Ustawiony dla lewej sekcji styl overflow: auto;

    ok. 1 min
    Polecenie z arkusza

    Dla lewej sekcji: szerokość 50%, wysokość 600 px, paski przewijania tylko w momencie, gdy zawartość nie mieści się w bloku.

    Szerokość 50% ma już kolumna siatki. Tu wysokość i overflow: auto - pasek pojawi się dopiero wtedy, gdy osiem wierszy ze zdjęciami przekroczy 600 px (R.4.9).

    Typowe błędy

    overflow: scroll pokazuje paski zawsze, także przy krótkiej zawartości - arkusz mówi „tylko w momencie”.

    styl.css CSS zmienione: 5 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    
    main {
        display: grid;
        grid-template-columns: 50% 40%;
    }
    
    #left {
        height: 600px;
        overflow: auto;
    }
    Podgląd strony po tym kroku (index.php)
  30. Krok 30: Sekcja prawa: tło, kolor, wysokość, margines, cień

    Kryteria: , , , ,

    R.4.2 Ustawiony kolor tła dla bloku nagłówkowego, prawej sekcji i stopki #880E4F

    R.4.3 Ustawiony biały kolor czcionki dla bloku nagłówkowego, prawej sekcji i stopki

    R.4.4 Ustawiona wysokość sekcji lewej 600px, prawej 500px oraz szerokość sekcji lewej 50%, prawej 40% (width lub grid-template-columns lub flex)

    R.4.6 Ustawione marginesy wewnętrzne (padding) dla bloku nagłówkowego i stopki 10px oraz ustawione marginesy zewnętrzne (margin): dla tabeli auto, dla prawej sekcji 35px i dla przynajmniej jednej kontrolki 5px

    R.4.8 Ustawiony dla prawej sekcji cień box-shadow: 10px 10px 15px DimGray;

    ok. 2 min
    Polecenie z arkusza

    Dla prawej sekcji: kolor tła #880E4F, biały kolor czcionki, szerokość 40%, wysokość 500 px, marginesy zewnętrzne 35 px, cień bloku o przesunięciu 10 px w obu osiach, rozmyciu 15 px i kolorze DimGray.

    Tło z tej samej zmiennej co nagłówek. Szerokość 40% daje kolumna siatki, wysokości obu sekcji są już ustawione (R.4.4). Cień bloku z tabeli 2 arkusza: box-shadow: 10px 10px 15px DimGray - przesunięcie w prawo, w dół, rozmycie, kolor (R.4.8).

    Typowe błędy

    text-shadow to cień tekstu, nie bloku.

    styl.css CSS zmienione: 8 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    
    main {
        display: grid;
        grid-template-columns: 50% 40%;
    }
    
    #left {
        height: 600px;
        overflow: auto;
    }
    
    #right {
        background-color: var(--accent);
        color: white;
        height: 500px;
        margin: 35px;
        box-shadow: 10px 10px 15px DimGray;
    }
    Podgląd strony po tym kroku (index.php)
  31. Krok 31: Tabela: wyśrodkowanie i szerokość

    Kryteria: ,

    R.4.5 Ustawiona szerokość selektora table oraz wszystkich kontrolek (pola edycyjne, lista rozwijana, przycisk) 70%

    R.4.6 Ustawione marginesy wewnętrzne (padding) dla bloku nagłówkowego i stopki 10px oraz ustawione marginesy zewnętrzne (margin): dla tabeli auto, dla prawej sekcji 35px i dla przynajmniej jednej kontrolki 5px

    ok. 1 min
    Polecenie z arkusza

    Dla selektora tabeli: marginesy zewnętrzne automatycznie wyliczane przez przeglądarkę, szerokość 70%.

    Selektor znacznika table (uwaga arkusza). margin: auto przy ustalonej szerokości dzieli wolne miejsce po równo na obie strony - tabela staje na środku sekcji (R.4.5, R.4.6).

    styl.css CSS zmienione: 5 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    
    main {
        display: grid;
        grid-template-columns: 50% 40%;
    }
    
    #left {
        height: 600px;
        overflow: auto;
    }
    
    #right {
        background-color: var(--accent);
        color: white;
        height: 500px;
        margin: 35px;
        box-shadow: 10px 10px 15px DimGray;
    }
    
    table {
        margin: auto;
        width: 70%;
    }
    Podgląd strony po tym kroku (index.php)
  32. Krok 32: Kontrolki formularza: margines i szerokość

    Kryteria: ,

    R.4.5 Ustawiona szerokość selektora table oraz wszystkich kontrolek (pola edycyjne, lista rozwijana, przycisk) 70%

    R.4.6 Ustawione marginesy wewnętrzne (padding) dla bloku nagłówkowego i stopki 10px oraz ustawione marginesy zewnętrzne (margin): dla tabeli auto, dla prawej sekcji 35px i dla przynajmniej jednej kontrolki 5px

    ok. 2 min
    Polecenie z arkusza

    Dla wszystkich kontrolek (pola edycyjne, lista rozwijana, przycisk) marginesy zewnętrzne 5 px, szerokość 70%.

    input, select, button - margines 5 px i szerokość 70% (R.4.5, R.4.6). Dodatkowo formularz jest siatką o jednej kolumnie (display: grid i justify-items: center): każdy element - etykieta, pole, lista, przycisk - dostaje własny wiersz i staje na środku. To zastępuje znaczniki <br> z wersji podstawowej: o układzie decyduje CSS, a HTML zostaje samą treścią.

    styl.css CSS zmienione: 12 linii
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    
    main {
        display: grid;
        grid-template-columns: 50% 40%;
    }
    
    #left {
        height: 600px;
        overflow: auto;
    }
    
    #right {
        background-color: var(--accent);
        color: white;
        height: 500px;
        margin: 35px;
        box-shadow: 10px 10px 15px DimGray;
    }
    
    table {
        margin: auto;
        width: 70%;
    }
    
    form {
        display: grid;
        justify-items: center;
    }
    
    input,
    select,
    button {
        margin: 5px;
        width: 70%;
    }
    Podgląd strony po tym kroku (index.php)
  33. Krok 33: Wiersz tabeli pod kursorem

    Kryteria:

    R.4.10 Gdy kursor znajdzie się na wierszu tabeli jego kolor tła zmienia się na #F8BBD0

    ok. 1 min
    Polecenie z arkusza

    Gdy kursor znajdzie się na wierszu tabeli jego kolor tła zmienia się na #F8BBD0.

    Pseudoklasa :hover na tr, kolor ze zmiennej --highlight (R.4.10). Działa na każdym wierszu tabeli, także na tych, które dopisze skrypt 1.

    styl.css CSS zmienione: 4 linie
    :root {
        --background: #FCE4EC;
        --accent: #880E4F;
        --highlight: #F8BBD0;
    }
    
    body {
        font-family: "Century Gothic", sans-serif;
        text-align: center;
        background-color: var(--background);
    }
    
    header,
    footer {
        background-color: var(--accent);
        color: white;
        padding: 10px;
        letter-spacing: 20px;
    }
    
    main {
        display: grid;
        grid-template-columns: 50% 40%;
    }
    
    #left {
        height: 600px;
        overflow: auto;
    }
    
    #right {
        background-color: var(--accent);
        color: white;
        height: 500px;
        margin: 35px;
        box-shadow: 10px 10px 15px DimGray;
    }
    
    table {
        margin: auto;
        width: 70%;
    }
    
    form {
        display: grid;
        justify-items: center;
    }
    
    input,
    select,
    button {
        margin: 5px;
        width: 70%;
    }
    
    tr:hover {
        background-color: var(--highlight);
    }
    Podgląd strony po tym kroku (index.php)
  34. Etap 4: Skrypty PHP

    Kroki 34-47 · po tym etapie masz 39 z 39 kryteriów (pkt)

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

    Kryteria:

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

    ok. 2 min
    Polecenie z arkusza

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

    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=szkolenia;charset=utf8mb4) to adres bazy dla PDO w jednym napisie: rodzaj serwera, host, nazwa bazy i kodowanie połączenia. Plik PHP bez HTML-u nie ma końcowego ?> - nic po nim nie wypisze się przypadkiem.

    Typowe błędy

    Egzaminator ocenia R.5.1 także w kodzie: jeśli dane połączenia są w config.php, ten plik musi być w folderze z rezultatami - bez niego strona się nie połączy.

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

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

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

    Kryteria: ,

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

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

    Kod PHP stoi na samym początku index.php, przed <!DOCTYPE html>: najpierw wszystkie operacje na bazie, potem szablon strony. Klucz R.5 przyjmuje PDO tak samo jak MySQLi.

    • require 'config.php' - dane z poprzedniego kroku trafiają do $config,
    • new PDO(...) - połączenie i wybór bazy szkolenia (R.5.1); opcje: ERRMODE_EXCEPTION - każdy błąd SQL zatrzymuje skrypt wyjątkiem, zamiast po cichu zwracać false, a FETCH_ASSOC - wiersze jako tablice z nazwami kolumn ($course['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 hasłem, ścieżką i nazwą serwera; 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 - to wskazówka dla atakującego. Szczegóły zapisuje się do dziennika błędów, nie na ekran.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  36. Krok 36: 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 wpisane w formularzu <script> wyświetli się jako tekst, a nie wykona (to ochrona przed XSS). Escapowanie robi się na wyjściu, tuż przed wypisaniem: do bazy idą dane surowe.

    Krótka nazwa to świadomy wyjątek od „znaczącego nazewnictwa” - e() występuje w szablonie kilkanaście razy i jest powszechnie znaną konwencją (tak samo nazywa się w Laravelu). Typy string przy parametrze i wyniku mówią, czego funkcja oczekuje.

    index.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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  37. Krok 37: Skrypt 1: zapytanie 1 do bazy

    Kryteria:

    R.5.3 Skrypt 1 zawiera instrukcję wysyłającą do bazy danych zapytanie 1 lub Skrypt 2 zawiera instrukcję wysyłającą do bazy danych zapytanie 2

    ok. 2 min
    Polecenie z arkusza

    Skrypt 1: wysyła do bazy danych zapytanie 1.

    Zapytanie 1 trafia do bloku try - wyjątek z dowolnego zapytania obsłuży ten sam catch, więc blok try obejmuje teraz połączenie i wszystkie operacje na bazie. $db->query(...) wysyła zapytanie (R.5.3), a fetchAll() od razu odbiera wszystkie wiersze do tablicy $courses. W zapytaniu nie ma danych od użytkownika, więc nie trzeba go przygotowywać - wystarcza query.

    Dane są gotowe, zanim zacznie się HTML; szablon niżej tylko je wypisze.

    index.php PHP zmienione: 2 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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  38. Krok 38: Skrypt 1: wiersz z obrazem dla każdego kursu

    Kryteria: ,

    R.2.6 W sekcji lewej umieszczona przynajmniej jedna grafika z tekstem alternatywnym "Kurs" (w skrypcie lub HTML)

    R.5.5 Skrypt 1 wyświetla w I kolumnie tabeli wszystkie obrazy, których nazwy to kody zwrócone z bazy, a rozszerzenia to .jpg

    ok. 3 min
    Polecenie z arkusza

    Dla każdego zwróconego przez bazę rekordu, w kolejnych wierszach tabeli wstawiane są: do pierwszej komórki: obraz o nazwie utworzonej ze zwróconego przez bazę kodu i rozszerzenia .jpg z tekstem alternatywnym: „kurs” (dla kodu 1 jest to 1.jpg).

    W <tbody> pętla w składni szablonu: foreach (...): i endforeach; zamiast nawiasów klamrowych. HTML wiersza zostaje zwykłym HTML-em z wcięciami, a PHP wstawia tylko wartości przez <?= ... ?> (skrót od <?php echo ... ?>). Nazwa pliku to kod z bazy i dopisane .jpg - dla kodu 5 wychodzi 5.jpg (R.5.5), tekst alternatywny kurs (R.2.6).

    Podgląd: osiem zdjęć, od gastronomii (najtańszej) do spawania.

    Typowe błędy

    $course['nazwa'] w miejscu kodu dałby gastronomia.jpg - plików nazwanych od kursów nie ma.

    index.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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  39. Krok 39: Skrypt 1: nazwa i cena w kolejnych komórkach

    Kryteria:

    R.5.4 Skrypt 1 dla każdego zwróconego przez bazę rekordu, wyświetla w II i III kolumnie tabeli kolejne nazwy i ceny kursów

    ok. 1 min
    Polecenie z arkusza

    Do kolejnych komórek: zwrócone z bazy nazwa i cena kursu.

    Dwie komórki, każda z wartością po nazwie kolumny - kolejność kolumn w SELECT przestaje mieć znaczenie (R.5.4). Nazwa przechodzi przez e(); cena to liczba, ale e() na każdej wartości to prostsza reguła niż zastanawianie się, która wartość jest bezpieczna. Cena zostaje w zapisie z bazy (1000, jak na ilustracji 2).

    Tabela wygląda jak na ilustracji 2, a pasek przewijania pojawia się sam - osiem wierszy przekracza 600 px.

    index.php PHP zmienione: 2 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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  40. Krok 40: Skrypt 2: zapytanie 2 do bazy

    Kryteria:

    R.5.3 Skrypt 1 zawiera instrukcję wysyłającą do bazy danych zapytanie 1 lub Skrypt 2 zawiera instrukcję wysyłającą do bazy danych zapytanie 2

    ok. 1 min
    Polecenie z arkusza

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

    Zapytanie 2 obok zapytania 1. PDO::FETCH_COLUMN zwraca zamiast listy wierszy listę wartości pierwszej kolumny - $courseNames to od razu ['język angielski', 'język niemiecki', …].

    index.php PHP zmienione: 1 linia
    <?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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  41. Krok 41: Skrypt 2: nazwy kursów jako opcje listy

    Kryteria:

    R.5.6 Skrypt 2 wyświetla nazwy kursów jako elementy listy rozwijanej w znaczniku <option>

    ok. 1 min
    Polecenie z arkusza

    Wszystkie zwrócone nazwy kursów są zapisywane jako elementy listy rozwijanej.

    Ta sama składnia szablonu co w tabeli - jeden <option> na nazwę (R.5.6). Lista ma 8 kursów od języka angielskiego, jak na ilustracji 4.

    index.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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  42. Krok 42: Skrypt 3: dane po kliknięciu „Dodaj dane”

    Kryteria:

    R.5.9 Skrypt zawiera instrukcję pobierającą dane z tablicy globalnej odpowiadającej zastosowanej metodzie formularza ($_POST, $_GET, $_REQUEST)

    ok. 3 min
    Polecenie z arkusza

    Skrypt 3: wykonywany po kliknięciu przycisku „Dodaj dane”.

    „Po kliknięciu przycisku” to żądanie metodą POST - sprawdza to $_SERVER['REQUEST_METHOD'] === 'POST'. Przy zwykłym wejściu na stronę (GET) blok się nie wykonuje.

    Wartości przychodzą oczyszczone już przy wejściu:

    • imię i nazwisko z $_POST (R.5.9) przez trim() - spacje na brzegach nie trafią do bazy, a pole z samymi spacjami staje się puste; ?? '' daje pusty napis, gdy pola w żądaniu w ogóle nie ma (zamiast ostrzeżenia),
    • wiek przez filter_input z filtrem FILTER_VALIDATE_INT i zakresem 1-120: wynik to liczba całkowita albo false dla „abc”, „36.5” czy „500”. Ten sam zakres ma pole w HTML-u, ale tamto sprawdzenie można ominąć - to w PHP decyduje.
    Typowe błędy

    $_POST['name'] bez ?? '' przy żądaniu bez tego pola daje ostrzeżenie „Undefined array key”.

    index.php PHP zmienione: 6 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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $name = trim($_POST['name'] ?? '');
            $surname = trim($_POST['surname'] ?? '');
            $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]);
        }
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  43. Krok 43: Skrypt 3: czy wprowadzono imię, nazwisko i wiek

    ok. 1 min
    Polecenie z arkusza

    Skrypt 3 działa tylko wtedy, gdy imię, nazwisko i wiek zostały wprowadzone w formularzu.

    Warunek na wartościach po oczyszczeniu: niepuste imię i nazwisko oraz wiek, który przeszedł filtr (false = zła wartość, null = brak pola). Porównania !== są ścisłe - nie ma pułapki, w której empty('0') uznaje zero za brak wartości.

    index.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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $name = trim($_POST['name'] ?? '');
            $surname = trim($_POST['surname'] ?? '');
            $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]);
    
            if ($name !== '' && $surname !== '' && $age !== false && $age !== null) {
            }
        }
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  44. Krok 44: Skrypt 3: zapytanie 3 przygotowane z danymi z formularza

    Kryteria:

    R.5.7 Skrypt 3 sprawdza czy dane zostały wysłane z formularza, jeśli tak, to wysyła do bazy danych zmodyfikowane zapytanie 3, z wstawionymi danymi z formularza oraz wyświetla tekst „Dane uczestnika <imię> <nazwisko> zostały dodane”, gdzie pola <> są wysłane z formularza (sposób alternatywny z atrybutem required, wstawienie rekordu sprawdzić w phpMyAdmin)

    ok. 3 min
    Polecenie z arkusza

    Wysyła do bazy danych zapytanie 3, zmodyfikowane tak, że dane wysłane z formularza są wstawione do zapytania.

    Zapytanie 3 jako zapytanie przygotowane: w miejscu wartości stoją parametry nazwane :name, :surname, :age, a dane idą osobno w execute([...]). Baza najpierw dostaje treść zapytania, potem same wartości - nazwisko Nowak'); DROP TABLE uczestnicy; -- zostanie zapisane jako dziwne nazwisko, a nie wykonane jako polecenie (SQL injection jest niemożliwe). Parametry nazwane czyta się łatwiej niż ? - widać, która wartość trafia w które miejsce.

    Klucz R.5.7 sprawdza, że do bazy idzie zapytanie 3 z danymi z formularza - przygotowane spełnia to tak samo jak sklejone. Podgląd: stan po wysłaniu formularza z ilustracji 5 (Magdalena Janowska, 35) - wiersz jest już w bazie.

    Typowe błędy

    Sklejanie "... VALUES ('$name', ...)" także w PDO jest podatne na SQL injection - zabezpiecza dopiero oddzielenie danych od zapytania.

    index.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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $name = trim($_POST['name'] ?? '');
            $surname = trim($_POST['surname'] ?? '');
            $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]);
    
            if ($name !== '' && $surname !== '' && $age !== false && $age !== null) {
                $insert = $db->prepare(
                    'INSERT INTO uczestnicy (imie, nazwisko, wiek) VALUES (:name, :surname, :age)'
                );
                $insert->execute(['name' => $name, 'surname' => $surname, 'age' => $age]);
            }
        }
    } 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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  45. Krok 45: Skrypt 3: komunikat i przekierowanie po zapisie (PRG)

    Kryteria:

    R.5.7 Skrypt 3 sprawdza czy dane zostały wysłane z formularza, jeśli tak, to wysyła do bazy danych zmodyfikowane zapytanie 3, z wstawionymi danymi z formularza oraz wyświetla tekst „Dane uczestnika <imię> <nazwisko> zostały dodane”, gdzie pola <> są wysłane z formularza (sposób alternatywny z atrybutem required, wstawienie rekordu sprawdzić w phpMyAdmin)

    ok. 4 min
    Polecenie z arkusza

    Wyświetla paragraf o treści „Dane uczestnika <imię> <nazwisko> zostały dodane”, gdzie pola <> oznaczają wartości wysłane z formularza.

    Po udanym INSERT strona nie wypisuje komunikatu od razu, tylko stosuje wzorzec PRG (Post/Redirect/Get):

    1. komunikat z imieniem i nazwiskiem trafia do sesji ($_SESSION['message'], sesję otwiera session_start() na początku pliku),
    2. header('Location: index.php', true, 303) każe przeglądarce otworzyć stronę od nowa zwykłym GET-em,
    3. przy tym wejściu $message odbiera komunikat z sesji i od razu go usuwa (unset) - pokaże się dokładnie raz, w paragrafie pod formularzem. Odbiór stoi przed obsługą formularza: w żądaniu POST sesja jest jeszcze pusta, a nowy komunikat czeka na następne wejście.

    Po co: bez przekierowania F5 po zapisie pyta „wysłać formularz ponownie?” i jedno kliknięcie dodaje Magdalenę Janowską drugi raz. Przekierowanie stoi za blokiem try - po wszystkich operacjach na bazie - i musi być przed jakimkolwiek HTML-em, bo nagłówek wysyła się przed treścią. W szablonie e($message), bo imię pochodzi od użytkownika.

    Podgląd: strona po przekierowaniu, z komunikatem „Dane uczestnika Magdalena Janowska zostały dodane” (R.5.7). Pola formularza są puste - to też skutek PRG, strona załadowała się od nowa.

    Typowe błędy

    header() po wypisaniu choćby spacji przed <?php nie zadziała („headers already sent”). Plik zaczyna się od <?php w pierwszej linii, bez pustej linii przed nim.

    index.php PHP zmienione: 13 linii
    <?php
    session_start();
    $config = require 'config.php';
    $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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    
        $message = $_SESSION['message'] ?? '';
        unset($_SESSION['message']);
    
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $name = trim($_POST['name'] ?? '');
            $surname = trim($_POST['surname'] ?? '');
            $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]);
    
            if ($name !== '' && $surname !== '' && $age !== false && $age !== null) {
                $insert = $db->prepare(
                    'INSERT INTO uczestnicy (imie, nazwisko, wiek) VALUES (:name, :surname, :age)'
                );
                $insert->execute(['name' => $name, 'surname' => $surname, 'age' => $age]);
                $_SESSION['message'] = "Dane uczestnika $name $surname zostały dodane";
                $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: index.php', true, 303);
        exit;
    }
    
    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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
                <p><?= e($message) ?></p>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  46. Krok 46: Skrypt 3: komunikat o brakujących danych

    Kryteria:

    R.5.8 Skrypt 3 w przeciwnym do R.5.7 przypadku wyświetla tekst „Wprowadź wszystkie dane”

    ok. 1 min
    Polecenie z arkusza

    W przeciwnym wypadku wyświetla paragraf „Wprowadź wszystkie dane”.

    Gdy w sesji nie ma komunikatu o zapisie, ?? podstawia „Wprowadź wszystkie dane” (R.5.8). Tak jest przy formularzu z pustym albo złym polem (brak przekierowania) i przy pierwszym wejściu na stronę - dlatego na ilustracji 2 ten napis widać od razu. Podgląd wraca do pierwszego wejścia.

    index.php PHP zmienione: 1 linia
    <?php
    session_start();
    $config = require 'config.php';
    $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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    
        $message = $_SESSION['message'] ?? 'Wprowadź wszystkie dane';
        unset($_SESSION['message']);
    
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $name = trim($_POST['name'] ?? '');
            $surname = trim($_POST['surname'] ?? '');
            $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]);
    
            if ($name !== '' && $surname !== '' && $age !== false && $age !== null) {
                $insert = $db->prepare(
                    'INSERT INTO uczestnicy (imie, nazwisko, wiek) VALUES (:name, :surname, :age)'
                );
                $insert->execute(['name' => $name, 'surname' => $surname, 'age' => $age]);
                $_SESSION['message'] = "Dane uczestnika $name $surname zostały dodane";
                $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: index.php', true, 303);
        exit;
    }
    
    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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
                <p><?= e($message) ?></p>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  47. Krok 47: 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 bazą.

    W PDO nie ma metody close() - połączenie zamyka się, gdy znika ostatnie odwołanie do obiektu, czyli po $db = null; (R.5.2). Stoi na końcu bloku try, po wszystkich zapytaniach i przed przekierowaniem - ostatnia operacja na bazie. Szablon niżej korzysta już tylko z gotowych tablic, połączenia nie potrzebuje.

    Typowe błędy

    $db->close() nie istnieje w PDO - to błąd „Call to undefined method”.

    index.php PHP zmienione: 2 linie
    <?php
    session_start();
    $config = require 'config.php';
    $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,
        ]);
    
        $courses = $db->query('SELECT kod, nazwa, cena FROM kursy ORDER BY cena')->fetchAll();
        $courseNames = $db->query('SELECT nazwa FROM kursy')->fetchAll(PDO::FETCH_COLUMN);
    
        $message = $_SESSION['message'] ?? 'Wprowadź wszystkie dane';
        unset($_SESSION['message']);
    
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $name = trim($_POST['name'] ?? '');
            $surname = trim($_POST['surname'] ?? '');
            $age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 120]]);
    
            if ($name !== '' && $surname !== '' && $age !== false && $age !== null) {
                $insert = $db->prepare(
                    'INSERT INTO uczestnicy (imie, nazwisko, wiek) VALUES (:name, :surname, :age)'
                );
                $insert->execute(['name' => $name, 'surname' => $surname, 'age' => $age]);
                $_SESSION['message'] = "Dane uczestnika $name $surname zostały dodane";
                $redirect = true;
            }
        }
    
        $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: index.php', true, 303);
        exit;
    }
    
    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>Szkolenia i kursy</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>SZKOLENIA</h1>
        </header>
        <main>
            <section id="left">
                <table>
                    <thead>
                        <tr>
                            <th>Kurs</th>
                            <th>Nazwa</th>
                            <th>Cena</th>
                        </tr>
                    </thead>
                    <tbody>
    <?php foreach ($courses as $course): ?>
                        <tr>
                            <td><img src="<?= e($course['kod']) ?>.jpg" alt="kurs"></td>
                            <td><?= e($course['nazwa']) ?></td>
                            <td><?= e($course['cena']) ?></td>
                        </tr>
    <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <section id="right">
                <h2>Zapisy na kursy</h2>
                <form method="post">
                    <label for="name">Imię</label>
                    <input type="text" id="name" name="name" autocomplete="given-name">
                    <label for="surname">Nazwisko</label>
                    <input type="text" id="surname" name="surname" autocomplete="family-name">
                    <label for="age">Wiek</label>
                    <input type="number" id="age" name="age" min="1" max="120">
                    <label for="course">Rodzaj kursu</label>
                    <select id="course" name="course">
    <?php foreach ($courseNames as $courseName): ?>
                        <option><?= e($courseName) ?></option>
    <?php endforeach; ?>
                    </select>
                    <button type="submit">Dodaj dane</button>
                </form>
                <p><?= e($message) ?></p>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

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

    Krok 48 · po tym etapie masz 39 z 39 kryteriów (pkt)

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

    ok. 2 min

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

    wersja podstawowawersja nowoczesna
    układdisplay: flex, szerokości w sekcjachdisplay: grid, szerokości w grid-template-columns
    formularz<br> po każdym elemenciesiatka w CSS, HTML bez <br>
    bazafunkcje mysqli_* z tabeli 1PDO, wyjątki, config.php
    INSERTdane sklejone z zapytaniemzapytanie przygotowane
    wypisywanieecho w pętli whileszablon foreach … endforeach i e()
    po zapisiekomunikat od razuPRG: przekierowanie i komunikat z sesji

    Na egzaminie: wersja podstawowa jest krótsza i używa tylko funkcji z tabeli 1 arkusza - mniej miejsc na pomyłkę, gdy brakuje czasu. Wersję nowoczesną pisz, jeśli znasz PDO na pamięć: arkusz nie podaje jego funkcji, a literówka w PDO::ATTR_ERRMODE to błąd, którego nikt nie podpowie. Gotowa aplikacja dostałaby jeszcze token CSRF w formularzu (ochrona przed wysłaniem formularza z cudzej strony) - klucz tego nie wymaga.

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

  49. Etap 6: Oddanie pracy

    Krok 49 · po tym etapie masz 39 z 39 kryteriów (pkt)

    Krok 49: 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 przeglądarka.txt nazwa przeglądarki, w której strona była sprawdzana przez http://localhost/…. W folderze z numerem zdającego: 1.jpg … 8.jpg, import.png, index.php, config.php (bez niego strona się nie połączy), kw1.jpg … kw4.jpg, kwerendy.txt, przeglądarka.txt, styl.css. Płytę sprawdź po nagraniu i opisz numerem zdającego.

    przeglądarka.txt Tekst nowy plik
    Google Chrome

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

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.