Przejdź do głównej treści
INF.03-11-25.06-SGCzerwiec 2025 · 51 kroków · ok. 83 min · 37 kryteriów z klucza

Rozwiązanie krok po kroku: Portal sprzedaży opon (Nowoczesny kod (grid, PDO))

To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza opony, cztery zapytania (w tym losowy rekord i UPDATE), opona z przezroczystym tłem, strona opony.php odświeżana co 10 sekund, style w styl.css - i tak samo spełnia każde kryterium klucza. Inna jest droga:

  • układ na siatce CSS: grid-template-areas rysuje ilustrację 3 wprost w kodzie (blok boczny na dwa wiersze, sekcje jedna pod drugą), szerokości 40% i 60% są w grid-template-columns, bez float i clear w układzie,
  • kolory z arkusza w zmiennych CSS, każda opona w znaczniku <article>, a pływająca ikona nie wystaje z bloku dzięki display: flow-root,
  • PHP na PDO z wyjątkami i danymi połączenia w osobnym pliku config.php; wszystkie zapytania wykonują się na początku pliku, a szablon niżej tylko wypisuje gotowe dane przez funkcję e(),
  • ikonę sezonu wybiera wyrażenie match zamiast drabinki if, a wartość zamówienia jest zapisana po polsku (number_format).

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 grafiki pochodzą z prawdziwego archiwum pliki8 (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 37 kryteriów (R.1.1 - R.5.10), każde za 1 punkt. Próg zdania części praktycznej to 75%, czyli 28 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 37 kryteriów (pkt)

    Krok 1: Baza danych opony

    Kryteria:

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

    ok. 2 min
    Polecenie z arkusza

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

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

    CREATE DATABASE opony CHARACTER SET utf8 COLLATE utf8_polish_ci;
    

    Trzy podobne nazwy: baza opony, plik do importu opona.sql i tabela opony w środku bazy.

    Typowe błędy

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

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

    Kryteria:

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

    ok. 2 min
    Polecenie z arkusza

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

    Zaznacz bazę opony w lewym panelu, karta Import, plik opona.sql, Wykonaj. Powstają trzy tabele z ilustracji 1: klient (9 klientów), opony (27 opon: nr_kat, producent, model, sezon, cena) i zamowienie (325 zamówień z dwoma kluczami obcymi: id wskazuje klienta, nr_kat oponę).

    Typowe błędy

    Import bez zaznaczonej bazy opony 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 opony , 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 wszystkie zrzuty są w PNG.

    Typowe błędy

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

  4. Krok 4: Zapytanie 1: dziesięć najtańszych opon

    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 pierwsze 10 rekordów z najtańszymi oponami (wszystkie pola). W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10; (lub SELECT * FROM opony ORDER BY cena LIMIT 10;) oraz wynik działania zapytania udokumentowany zrzutem, na którym widocznych jest dokładnie 10 rekordów z wszystkimi polami

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 1: wybierające pierwsze 10 rekordów z najtańszymi oponami (wszystkie pola).

    Każda klauzula w osobnej linii - zapytanie czyta się od góry: co (SELECT z pięcioma kolumnami), skąd (FROM opony), w jakiej kolejności (ORDER BY cena ASC) i ile (LIMIT 10).

    „Wszystkie pola” można zapisać gwiazdką (klucz przyjmuje obie formy), ale jawna lista kolumn ma przewagę w kodzie strony: kolejność kolumn jest pewna, a dopisanie kiedyś do tabeli nowej kolumny (np. opisu na kilka tysięcy znaków) nie spowolni strony, która jej nie używa.

    Wynik: 10 opon, od uLFA AL135 za 101,50 (R.1.3). Zapytanie do kwerendy.txt, zrzut kw1.png. Zrzuty zapytań 1-3 rób przed zapytaniem 4, które zmieni ceny.

    Typowe błędy

    LIMIT 10 bez ORDER BY cena daje dziesięć pierwszych opon z tabeli, nie dziesięć najtańszych.

    kwerendy.txt Tekst nowy plik
    -- Zapytanie 1
    SELECT nr_kat, producent, model, sezon, cena
    FROM opony
    ORDER BY cena ASC
    LIMIT 10;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 4
  5. Krok 5: Zapytanie 2: opona o numerze katalogowym 9

    Kryteria:

    R.1.4 Wykonane zapytanie 2 wybierające jedynie producenta, model, sezon i cenę opony o numerze katalogowym 9. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = 9; oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczny jest dokładnie jeden rekord z danymi: uLFA, AL135 zimowa; oraz widoczna jest cena

    ok. 1 min
    Polecenie z arkusza

    Zapytanie 2: wybierające jedynie producenta, model, sezon i cenę opony o numerze katalogowym 9.

    Cztery kolumny w kolejności z arkusza i warunek na klucz główny - wynik to jeden wiersz: uLFA, AL135, zimowa, 101,50 (R.1.4). Zrzut kw2.png.

    kwerendy.txt Tekst zmienione: 5 linii
    -- Zapytanie 1
    SELECT nr_kat, producent, model, sezon, cena
    FROM opony
    ORDER BY cena ASC
    LIMIT 10;
    
    -- Zapytanie 2
    SELECT producent, model, sezon, cena
    FROM opony
    WHERE nr_kat = 9;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 5
  6. Krok 6: Zapytanie 3: jedno losowe zamówienie z modelem i ceną

    Kryteria:

    R.1.5 Wykonane zapytanie 3 wybierające jedynie identyfikator zamówienia i ilość oraz odpowiadające im model i cenę opony. Wybierany jest tylko jeden losowy rekord. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT id_zam, ilosc, model, cena FROM zamowienie JOIN opony USING (nr_kat) ORDER BY RAND() LIMIT 1; (lub porównanie kluczy po ON lub WHERE) oraz wynik działania zapytania udokumentowany zrzutem, na którym widoczny jest dokładnie jeden rekord z 4 polami

    ok. 3 min
    Polecenie z arkusza

    Zapytanie 3: wybierające jedynie identyfikator zamówienia i ilość oraz odpowiadające im model i cenę opony. Wybierany jest tylko jeden losowy rekord. Należy zastosować relację.

    Złączenie z aliasami tabel: zamowienie AS z, opony AS o. Każda kolumna mówi, z której tabeli pochodzi (z.ilosc, o.model), więc nie trzeba pamiętać, że nr_kat jest w obu tabelach, a reszta nazw akurat się nie powtarza.

    • JOIN opony AS o ON o.nr_kat = z.nr_kat - relacja z ilustracji 1: do zamówienia dołączona jego opona,
    • ORDER BY RAND() i LIMIT 1 - przepis z ramki w arkuszu: wiersze w losowej kolejności, z nich pierwszy.

    Wynik: jeden wiersz z czterema polami (R.1.5), przy każdym wykonaniu inne zamówienie. Na zrzucie kw3.png zamówienie 7: 4 sztuki modelu AL100 po 290,50.

    Typowe błędy

    Złączenie bez warunku (FROM zamowienie, opony bez WHERE) łączy każde zamówienie z każdą oponą - losowy wiersz ma wtedy cenę obcej opony.

    kwerendy.txt Tekst zmienione: 7 linii
    -- Zapytanie 1
    SELECT nr_kat, producent, model, sezon, cena
    FROM opony
    ORDER BY cena ASC
    LIMIT 10;
    
    -- Zapytanie 2
    SELECT producent, model, sezon, cena
    FROM opony
    WHERE nr_kat = 9;
    
    -- Zapytanie 3
    SELECT z.id_zam, z.ilosc, o.model, o.cena
    FROM zamowienie AS z
    JOIN opony AS o ON o.nr_kat = z.nr_kat
    ORDER BY RAND()
    LIMIT 1;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 6
  7. Krok 7: Zapytanie 4: obniżka cen opon letnich o 25%

    Kryteria:

    R.1.6 Wykonane zapytanie 4 aktualizujące cenę opon letnich obniżając ją o 25%. W pliku z kwerendami lub na zrzucie istnieje zapis np.: UPDATE opony SET cena = cena - cena * 0.25 WHERE sezon = "letnia"; (opcjonalnie cena = 0.75 * cena) oraz wynik działania zapytania udokumentowany zrzutem, na którym jest komunikat o pomyślnym wykonaniu zapytania lub widoczna tabela ze zmienionymi wartościami

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 4: aktualizujące cenę opon letnich obniżając ją o 25%.

    UPDATE zmienia dane: cena po obniżce o 25% to 75% ceny, więc cena * 0.75. Warunek WHERE sezon = 'letnia' ogranicza zmianę do opon letnich - bez niego przecena objęłaby wszystkie 27 opon.

    Napis w SQL zapisany jest w apostrofach ('letnia'). MySQL przyjmuje też cudzysłów (tak pokazuje klucz), ale w standardzie SQL cudzysłów oznacza nazwę kolumny, a nie tekst - apostrofy działają w każdej bazie.

    Komunikat „Zmodyfikowanych rekordów: 17” to zrzut kw4.png (R.1.6).

    Typowe błędy

    Dwukrotne wykonanie UPDATE przecenia opony dwa razy. Przed ponownym uruchomieniem przywróć tabelę ponownym importem.

    kwerendy.txt Tekst zmienione: 5 linii
    -- Zapytanie 1
    SELECT nr_kat, producent, model, sezon, cena
    FROM opony
    ORDER BY cena ASC
    LIMIT 10;
    
    -- Zapytanie 2
    SELECT producent, model, sezon, cena
    FROM opony
    WHERE nr_kat = 9;
    
    -- Zapytanie 3
    SELECT z.id_zam, z.ilosc, o.model, o.cena
    FROM zamowienie AS z
    JOIN opony AS o ON o.nr_kat = z.nr_kat
    ORDER BY RAND()
    LIMIT 1;
    
    -- Zapytanie 4
    UPDATE opony
    SET cena = cena * 0.75
    WHERE sezon = 'letnia';
    Podgląd strony po tym kroku
    Podgląd strony po kroku 7
  8. Etap 2: Grafika i strona HTML

    Kroki 8-20 · po tym etapie masz 15 z 37 kryteriów (pkt)

    Krok 8: Grafika opona.png: przezroczyste tło

    Kryteria:

    R.2.1 Grafika opona.png ma przezroczyste tło i widoczna jest opona

    ok. 4 min
    Polecenie z arkusza

    Cechy grafiki opona.png: wykonana na podstawie grafiki opona.jpg i zapisana w formacie PNG. Tło przezroczyste.

    W GIMP-ie: opona.jpg, Warstwa → Przezroczystość → Dodaj kanał alfa (JPEG nie ma przezroczystości), Zaznaczenie rozmyte z progiem ok. 20, klik w białe tło, Delete. Eksport do opona.png - tylko PNG zachowa kanał alfa (R.2.1). Białe odblaski na feldze nie łączą się z tłem, więc zostają.

    Typowe błędy

    Eksport do JPEG zamienia przezroczystość z powrotem w białe tło.

    Podgląd strony po tym kroku
    Podgląd strony po kroku 8
  9. Krok 9: Grafika opona.png: wysokość 250 px

    Kryteria:

    R.2.2 Grafika przeskalowana do wysokości 250 px, odpowiadająca temu szerokość wynosi 178 px (± 5 px)

    ok. 2 min
    Polecenie z arkusza

    Przeskalowana z zachowaniem proporcji do wysokości 250 px.

    Obraz → Skaluj obraz, wysokość 250 px przy zamkniętym łańcuchu proporcji; szerokość wychodzi 178 px (R.2.2: 178 ±5). Skalowanie po usunięciu tła - różdżka pracuje dokładniej na pełnej rozdzielczości.

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

  10. Krok 10: Plik opony.php w HTML5

    Kryteria:

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

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

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

    Kryteria:

    R.2.3 W pliku opony.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.

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

    Kryteria:

    R.2.3 W pliku opony.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.

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

    Kryteria:

    R.2.4 Stronie nadany tytuł "OPONY”

    ok. 1 min
    Polecenie z arkusza

    Tytuł strony „OPONY”.

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

    opony.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>OPONY</title>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  14. Krok 14: Podpięcie arkusza stylów

    Kryteria:

    R.3.2 Strona zawiera działające połączenie z zewnętrznym arkuszem stylów o nazwie styl.css formatowanie CSS pochodzi jedynie z tego arkusza

    ok. 1 min
    Polecenie z arkusza

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

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

    Typowe błędy

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

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

    Kryteria:

    R.2.5 W sekcji body zdefiniowany jest podział na bloki za pomocą semantycznych znaczników HTML 5: main->aside+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: blok boczny, sekcja 1 i sekcja 2.

    Struktura z ilustracji 3 i klucza R.2.5: main z aside i dwiema sekcjami, pod nim footer. Kolejność w kodzie odpowiada kolejności czytania - blok boczny pierwszy, bo jest pierwszy z lewej; o tym, że stanie obok sekcji, zdecyduje CSS.

    Sekcje dostają klasy offer i order (nie identyfikatory): klasa opisuje rolę bloku i ma niską „wagę” w CSS, więc później łatwo ją nadpisać. Nazwy po angielsku; nazwy narzucone przez arkusz (opony.php, styl.css, klasa opona, kolumny bazy) zostają.

    Typowe błędy

    <aside> poza <main> nie spełnia R.2.5 - blok boczny jest częścią bloku głównego.

    opony.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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
            </aside>
            <section class="offer">
            </section>
            <section class="order">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  16. Krok 16: Blok boczny: odnośnik „więcej ofert”

    Kryteria:

    R.3.3 Utworzony odnośnik o treści „więcej ofert” jego kliknięcie otwiera stronę https://opona.pl/

    ok. 1 min
    Polecenie z arkusza

    Zawartość bloku bocznego: efekt działania skryptu 1. Paragraf z odnośnikiem o treści „więcej ofert” prowadzący do strony https://opona.pl/.

    Paragraf z odnośnikiem pod pełny adres z https:// (R.3.3). Bloki z oponami (skrypt 1) staną nad nim. Odnośnik otwiera się w tej samej karcie - arkusz nie każe otwierać nowej, a decyzję o nowej karcie lepiej zostawić użytkownikowi.

    Typowe błędy

    href="opona.pl" bez https:// to ścieżka do pliku w folderze strony, nie adres serwisu.

    opony.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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
            </section>
            <section class="order">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  17. Krok 17: Sekcja 1: grafika opony

    Kryteria:

    R.2.7 W pierwszej sekcji umieszczona grafika opona z tekstem alternatywnym "Opona"

    ok. 1 min
    Polecenie z arkusza

    Zawartość sekcji 1: grafika opona.png z tekstem alternatywnym o treści: „Opona”.

    <img> z przygotowanym PNG i tekstem alternatywnym z arkusza (R.2.7). Wymiarów w HTML nie trzeba - plik ma już 178 × 250 px.

    opony.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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
            </section>
            <section class="order">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  18. Krok 18: Sekcja 1: nagłówek „Opona dnia”

    Kryteria:

    R.2.6 Umieszczone w pierwszej sekcji <h2>: „Opona dnia”, w drugiej sekcji <h2>: „Najnowsze zamówienie", oraz paragraf z numerem zdającego w bloku stopki

    ok. 1 min
    Polecenie z arkusza

    Nagłówek drugiego stopnia o treści: „Opona dnia”. Efekt działania skryptu 2.

    <h2> pod obrazem; trzy kolejne <h2> z danymi opony numer 9 dopisze szablon PHP.

    opony.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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  19. Krok 19: Sekcja 2: nagłówek „Najnowsze zamówienie”

    Kryteria:

    R.2.6 Umieszczone w pierwszej sekcji <h2>: „Opona dnia”, w drugiej sekcji <h2>: „Najnowsze zamówienie", oraz paragraf z numerem zdającego w bloku stopki

    ok. 1 min
    Polecenie z arkusza

    Zawartość sekcji 2: nagłówek drugiego stopnia o treści: „Najnowsze zamówienie”. Efekt działania skryptu 3.

    <h2> w drugiej sekcji; dwa kolejne z losowym zamówieniem dopisze PHP.

    opony.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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  20. Krok 20: Stopka z numerem zdającego

    Kryteria:

    R.2.6 Umieszczone w pierwszej sekcji <h2>: „Opona dnia”, w drugiej sekcji <h2>: „Najnowsze zamówienie", oraz paragraf z numerem zdającego w bloku stopki

    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 wpisujesz swój numer zdającego (PESEL), bo tego szuka klucz R.2.6.

    opony.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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  21. Etap 3: Styl CSS

    Kroki 21-31 · po tym etapie masz 26 z 37 kryteriów (pkt)

    Krok 21: Zmienne CSS z kolorami arkusza

    ok. 2 min
    Polecenie z arkusza

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

    Plik styl.css zaczyna się od własnych właściwości (zmiennych CSS) w :root. Arkusz używa czterech odcieni granatu, a dwa z nich powtarzają się: #9FA8DA (sekcja 1 i bloki opon) i #7986CB (sekcja 2 i nagłówki h3). Wpisane raz, w regułach występują jako var(--light-bg) - zmiana palety to zmiana jednej linii.

    Nazwy opisują rolę koloru (jasne tło, ciemne tło), a nie sam kolor. Egzaminator w inspektorze widzi obliczoną wartość (rgb(159, 168, 218)), więc kryteria kolorów sprawdza tak samo.

    styl.css CSS nowy plik
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    Podgląd strony po tym kroku (opony.php)
  22. Krok 22: Krój czcionki dla wszystkich selektorów

    Kryteria:

    R.4.1 Ustawiony domyślnie dla wszystkich selektorów (*): krój czcionki "Trebuchet MS", sans- serif (tylko taka kolejność)

    ok. 1 min
    Polecenie z arkusza

    Domyślnie dla wszystkich selektorów: krój czcionki Trebuchet MS, w przypadku braku sans-serif.

    Selektor uniwersalny *, nazwa z dwóch słów w cudzysłowie, potem rodzina zastępcza sans-serif - klucz R.4.1 wymaga dokładnie tej kolejności.

    styl.css CSS zmienione: 4 linie
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    Podgląd strony po tym kroku (opony.php)
  23. Krok 23: Układ bloków zgodny z ilustracją 3: siatka

    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)

    R.4.4 Ustawiona wysokość bloku bocznego 600px obu sekcji 300px oraz szerokość bloku bocznego 40%, obu sekcji 60% (width lub grid-template-columns lub flex)

    ok. 3 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.

    Blok główny staje się siatką (display: grid) o dwóch kolumnach: 40% i 60% - to są szerokości bloku bocznego i sekcji z arkusza (klucz R.4.4 przyjmuje je wprost w grid-template-columns).

    grid-template-areas to mapa ilustracji 3 zapisana tekstem: każdy napis w cudzysłowie to wiersz siatki, każde słowo to komórka. Słowo side stoi w obu wierszach, więc blok boczny zajmie lewą kolumnę na całą wysokość, a offer i order ułożą się jedna pod drugą po prawej. Trzy krótkie reguły przypisują bloki do obszarów (grid-area).

    W wersji podstawowej ten sam układ robi float: left i clear: both - tam druga sekcja „spada” pod pierwszą, bo nie mieści się w wierszu. Siatka mówi wprost, co gdzie stoi, i nie zależy od tego, czy szerokości sumują się co do piksela (R.3.1).

    Typowe błędy

    width: 60% na sekcji wewnątrz siatki liczy się od szerokości komórki, nie strony - sekcja zajęłaby 60% z 60%. Szerokości trzymaj w grid-template-columns.

    styl.css CSS zmienione: 20 linii
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
    }
    
    .offer {
        grid-area: offer;
    }
    
    .order {
        grid-area: order;
    }
    Podgląd strony po tym kroku (opony.php)
  24. Krok 24: Blok boczny: tło, wysokość, paski przewijania

    Kryteria: , ,

    R.4.2 Ustawiony kolor tła dla bloku bocznego #C5CAE9, pierwszej sekcji #9FA8DA, drugiej sekcji i selektora h3 #7986CB, stopki #283593

    R.4.4 Ustawiona wysokość bloku bocznego 600px obu sekcji 300px oraz szerokość bloku bocznego 40%, obu sekcji 60% (width lub grid-template-columns lub flex)

    R.4.7 Ustawiona dla bloku bocznego cecha overflow: auto;

    ok. 1 min
    Polecenie z arkusza

    Dla bloku bocznego: kolor tła #C5CAE9, szerokość 40%, wysokość 600 px, paski przewijania tylko w przypadku przepełnienia bloku.

    Szerokość 40% już jest (pierwsza kolumna siatki). Dochodzą: wysokość 600 px (dwie sekcje po 300 px), overflow: auto - pasek przewijania pojawia się tylko, gdy treść się nie mieści (R.4.7), i tło ze zmiennej (R.4.2). Dziesięć bloków z oponami przekroczy 600 px, więc pasek będzie widoczny jak na ilustracji 2.

    styl.css CSS zmienione: 3 linie
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    .offer {
        grid-area: offer;
    }
    
    .order {
        grid-area: order;
    }
    Podgląd strony po tym kroku (opony.php)
  25. Krok 25: Obie sekcje: wysokość

    Kryteria:

    R.4.4 Ustawiona wysokość bloku bocznego 600px obu sekcji 300px oraz szerokość bloku bocznego 40%, obu sekcji 60% (width lub grid-template-columns lub flex)

    ok. 1 min
    Polecenie z arkusza

    Dla obu sekcji: szerokość 60%, wysokość 300 px.

    Selektor section obejmuje obie sekcje: 300 px wysokości. Szerokość 60% daje druga kolumna siatki - osobne width byłoby tu błędem (patrz poprzedni krok). Klucz R.4.4 wymienia grid-template-columns jako dopuszczalny sposób.

    styl.css CSS zmienione: 4 linie
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
    }
    
    .order {
        grid-area: order;
    }
    Podgląd strony po tym kroku (opony.php)
  26. Krok 26: Sekcje: osobne kolory tła

    Kryteria:

    R.4.2 Ustawiony kolor tła dla bloku bocznego #C5CAE9, pierwszej sekcji #9FA8DA, drugiej sekcji i selektora h3 #7986CB, stopki #283593

    ok. 1 min
    Polecenie z arkusza

    Dodatkowo kolor tła sekcji 1: #9FA8DA, sekcji 2: #7986CB.

    Tła trafiają do reguł klas .offer i .order, które już ustawiają obszar siatki - wszystko o danej sekcji jest w jednym miejscu (R.4.2). Przez przezroczyste tło opony prześwituje teraz fiolet sekcji.

    styl.css CSS zmienione: 2 linie
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
        background-color: var(--light-bg);
    }
    
    .order {
        grid-area: order;
        background-color: var(--medium-bg);
    }
    Podgląd strony po tym kroku (opony.php)
  27. Krok 27: Stopka: tło, kolor, margines wewnętrzny

    Kryteria: , ,

    R.4.2 Ustawiony kolor tła dla bloku bocznego #C5CAE9, pierwszej sekcji #9FA8DA, drugiej sekcji i selektora h3 #7986CB, stopki #283593

    R.4.3 Ustawiony biały kolor czcionki dla stopki i nagłówka trzeciego stopnia

    R.4.6 Ustawione marginesy wewnętrzne (padding) dla stopki oraz selektora img 20px

    ok. 1 min
    Polecenie z arkusza

    Dla bloku stopki: kolor tła #283593, biały kolor czcionki, marginesy wewnętrzne 20 px.

    Stopka jest zwykłym blokiem pod siatką - nie potrzebuje clear, bo nic obok niej nie pływa (R.4.2, R.4.3, R.4.6).

    styl.css CSS zmienione: 6 linii
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
        background-color: var(--light-bg);
    }
    
    .order {
        grid-area: order;
        background-color: var(--medium-bg);
    }
    
    footer {
        padding: 20px;
        background-color: var(--dark-bg);
        color: var(--light-text);
    }
    Podgląd strony po tym kroku (opony.php)
  28. Krok 28: Klasa opona: tło, marginesy, obramowanie, zaokrąglenie

    Kryteria:

    R.4.10 Zdefiniowana klasa dla opony oraz jej styl: kolor tła #9FA8DA, margin: 40px; border: 2px dotted #283593; zaokrąglenie rogów 30px

    ok. 2 min
    Polecenie z arkusza

    Dla klasy CSS opona, przypisanej do bloków generowanych w skrypcie 1: kolor tła #9FA8DA, marginesy zewnętrzne 40 px, obramowanie linią kropkowaną o szerokości 2 px i kolorze #283593, zaokrąglenie rogów 30 px.

    Nazwa klasy narzucona przez arkusz. Obramowanie skrótem border: 2px dotted … (szerokość, styl „kropkowana”, kolor), border-radius: 30px i tło (R.4.10).

    Dodatkowo display: flow-root: ikona sezonu w bloku będzie pływać w lewo (float z następnego kroku), a pływający element nie rozciąga rodzica - przy krótkim tekście wystawałby poza zaokrągloną ramkę. flow-root sprawia, że blok obejmuje pływające dzieci. To następca dawnego triku clearfix z pustym elementem i clear: both.

    Typowe błędy

    dashed to linia kreskowana; arkusz chce kropkowanej - dotted.

    styl.css CSS zmienione: 8 linii
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
        background-color: var(--light-bg);
    }
    
    .order {
        grid-area: order;
        background-color: var(--medium-bg);
    }
    
    footer {
        padding: 20px;
        background-color: var(--dark-bg);
        color: var(--light-text);
    }
    
    .opona {
        display: flow-root;
        margin: 40px;
        border: 2px dotted var(--dark-bg);
        border-radius: 30px;
        background-color: var(--light-bg);
    }
    Podgląd strony po tym kroku (opony.php)
  29. Krok 29: Obraz: opływanie i margines wewnętrzny

    Kryteria: ,

    R.4.6 Ustawione marginesy wewnętrzne (padding) dla stopki oraz selektora img 20px

    R.4.8 Ustawione dla obrazu opływanie tekstem po prawej stronie float: left;

    ok. 1 min
    Polecenie z arkusza

    Dla selektora obrazu: opływanie tekstem po prawej stronie (obraz jest po lewej stronie), marginesy wewnętrzne 20 px.

    Tu zostaje float: left: arkusz wymaga opływania tekstem, a klucz R.4.8 sprawdza właśnie tę właściwość. To jest zresztą jedyne zadanie, do którego float powstał - obraz w tekście, a nie układ strony. Selektor znacznika img, bez klas (uwaga arkusza). Reguła działa i na oponę w sekcji 1, i na ikony sezonów w blokach (R.4.6, R.4.8).

    styl.css CSS zmienione: 5 linii
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
        background-color: var(--light-bg);
    }
    
    .order {
        grid-area: order;
        background-color: var(--medium-bg);
    }
    
    footer {
        padding: 20px;
        background-color: var(--dark-bg);
        color: var(--light-text);
    }
    
    .opona {
        display: flow-root;
        margin: 40px;
        border: 2px dotted var(--dark-bg);
        border-radius: 30px;
        background-color: var(--light-bg);
    }
    
    img {
        float: left;
        padding: 20px;
    }
    Podgląd strony po tym kroku (opony.php)
  30. Krok 30: Nagłówki drugiego stopnia

    Kryteria:

    R.4.5 Ustawione dla selektora h2 wyrównanie tekstu do środka oraz margines zewnętrzny (margin) 0 i margines wewnętrzny (padding) 15px

    ok. 1 min
    Polecenie z arkusza

    Dla selektora nagłówka drugiego stopnia: wyrównanie tekstu do środka, marginesy zewnętrzne 0 px, marginesy wewnętrzne 15 px.

    Selektor h2 (R.4.5). Bez domyślnych marginesów cztery nagłówki sekcji 1 zmieszczą się w 300 px obok opony.

    styl.css CSS zmienione: 6 linii
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
        background-color: var(--light-bg);
    }
    
    .order {
        grid-area: order;
        background-color: var(--medium-bg);
    }
    
    footer {
        padding: 20px;
        background-color: var(--dark-bg);
        color: var(--light-text);
    }
    
    .opona {
        display: flow-root;
        margin: 40px;
        border: 2px dotted var(--dark-bg);
        border-radius: 30px;
        background-color: var(--light-bg);
    }
    
    img {
        float: left;
        padding: 20px;
    }
    
    h2 {
        margin: 0;
        padding: 15px;
        text-align: center;
    }
    Podgląd strony po tym kroku (opony.php)
  31. Krok 31: Nagłówki trzeciego stopnia

    Kryteria: , ,

    R.4.2 Ustawiony kolor tła dla bloku bocznego #C5CAE9, pierwszej sekcji #9FA8DA, drugiej sekcji i selektora h3 #7986CB, stopki #283593

    R.4.3 Ustawiony biały kolor czcionki dla stopki i nagłówka trzeciego stopnia

    R.4.9 Ustawiony dla nagłówka trzeciego stopnia rozmiar czcionki 140%

    ok. 1 min
    Polecenie z arkusza

    Dla selektora nagłówka trzeciego stopnia: kolor tła #7986CB, biały kolor czcionki, rozmiar czcionki 140%.

    Selektor h3 (R.4.2, R.4.3, R.4.9). Nagłówków h3 jeszcze nie ma - wypisze je szablon PHP z ceną każdej opony. Kolor tła to ta sama zmienna co tło sekcji 2.

    styl.css CSS zmienione: 6 linii
    :root {
        --side-bg: #C5CAE9;
        --light-bg: #9FA8DA;
        --medium-bg: #7986CB;
        --dark-bg: #283593;
        --light-text: white;
    }
    
    * {
        font-family: "Trebuchet MS", sans-serif;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
        grid-template-areas:
            "side offer"
            "side order";
    }
    
    aside {
        grid-area: side;
        height: 600px;
        overflow: auto;
        background-color: var(--side-bg);
    }
    
    section {
        height: 300px;
    }
    
    .offer {
        grid-area: offer;
        background-color: var(--light-bg);
    }
    
    .order {
        grid-area: order;
        background-color: var(--medium-bg);
    }
    
    footer {
        padding: 20px;
        background-color: var(--dark-bg);
        color: var(--light-text);
    }
    
    .opona {
        display: flow-root;
        margin: 40px;
        border: 2px dotted var(--dark-bg);
        border-radius: 30px;
        background-color: var(--light-bg);
    }
    
    img {
        float: left;
        padding: 20px;
    }
    
    h2 {
        margin: 0;
        padding: 15px;
        text-align: center;
    }
    
    h3 {
        font-size: 140%;
        background-color: var(--medium-bg);
        color: var(--light-text);
    }
    Podgląd strony po tym kroku (opony.php)
  32. Etap 4: Skrypty PHP

    Kroki 32-49 · po tym etapie masz 37 z 37 kryteriów (pkt)

    Krok 32: Odświeżanie strony co 10 sekund

    Kryteria:

    R.5.1 Strona jest odświeżana co 10 sekund

    ok. 2 min
    Polecenie z arkusza

    Napisane w języku PHP. Strona jest odświeżana co 10 sekund.

    header('refresh: 10') z ramki w arkuszu wysyła nagłówek HTTP „odśwież za 10 sekund” (R.5.1). Nagłówki idą przed treścią strony, dlatego cały kod PHP tej wersji stanie na samym początku pliku, przed <!DOCTYPE html> - i nic, nawet pusta linia, nie może stać przed <?php.

    Od tego kroku podgląd to prawdziwy wynik strony z serwera (odświeżania w podglądzie nie widać).

    Typowe błędy

    header() po wypisaniu czegokolwiek kończy się ostrzeżeniem „headers already sent” i strona się nie odświeża.

    opony.php PHP zmienione: 3 linie
    <?php
    header('refresh: 10');
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  33. Krok 33: Dane połączenia w pliku config.php

    Kryteria:

    R.5.2 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy opony oraz ostatnią operacją na bazie jest jej zamknięcie

    ok. 2 min
    Polecenie z arkusza

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

    Dane logowania do bazy w osobnym pliku config.php, który tylko zwraca tablicę (return [...]). Na prawdziwym serwerze hasło jest inne niż na komputerze programisty i nie trafia do repozytorium z kodem - podmienia się jeden plik. Arkusz dopuszcza dodatkowe pliki („ewentualnie inne przygotowane pliki”).

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

    Typowe błędy

    config.php musi trafić do folderu 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=opony;charset=utf8mb4',
        'user' => 'root',
        'password' => '',
    ];

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

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

    Kryteria: ,

    R.5.2 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy opony oraz ostatnią operacją na bazie jest jej zamknięcie

    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

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

    Klucz R.5 przyjmuje PDO tak samo jak MySQLi (R.5.2, R.5.10).

    • require 'config.php' - dane z poprzedniego kroku trafiają do $config,
    • new PDO(...) - połączenie i wybór bazy opony; opcja ERRMODE_EXCEPTION zamienia każdy błąd SQL w wyjątek (zamiast cichego false), a FETCH_ASSOC sprawia, że wiersze przychodzą jako tablice z nazwami kolumn ($tyre['cena'], nie $row[4]),
    • try … catch (PDOException $error) - gdy baza nie działa, użytkownik widzi zwykły komunikat zamiast treści błędu z nazwą serwera; http_response_code(500) mówi przeglądarce, że zawiódł serwer.
    Typowe błędy

    Wypisanie $error->getMessage() na ekran zdradza szczegóły serwera. Komunikat dla użytkownika ma być ogólny.

    opony.php PHP zmienione: 12 linii
    <?php
    header('refresh: 10');
    
    $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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  35. Krok 35: 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, który trafi do HTML-u, przejdzie przez e() (od escape). Strona nie ma formularza, ale nazwy producentów i modeli ktoś kiedyś wpisał do bazy - napis <script> w nazwie modelu wykonałby się u każdego odwiedzającego (XSS). htmlspecialchars z ENT_QUOTES zamienia <, >, & i oba cudzysłowy na encje, więc taki napis pokaże się jako tekst. Escapuje się na wyjściu; w bazie zostają dane surowe.

    Krótka nazwa to znana konwencja (tak samo w Laravelu) - funkcja występuje w szablonie wiele razy.

    opony.php PHP zmienione: 5 linii
    <?php
    header('refresh: 10');
    
    $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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  36. Krok 36: 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 - zapytanie 2 lub skrypt 3 - zapytanie 3

    ok. 2 min
    Polecenie z arkusza

    Skrypt 1 wysyła do bazy danych zapytanie 1.

    Zapytanie 1 trafia do bloku try, zaraz po połączeniu - ten sam catch obsłuży błąd każdego zapytania. $db->query(...) wysyła zapytanie (R.5.3), a fetchAll() odbiera od razu wszystkie dziesięć wierszy do tablicy $cheapestTyres. W zapytaniu nie ma danych od użytkownika, więc nie trzeba go przygotowywać.

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

    opony.php PHP zmienione: 4 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  37. Krok 37: Skrypt 1: blok z klasą opona dla każdego rekordu

    Kryteria: ,

    R.2.8 W bloku bocznym utworzony przynajmniej jeden blok z klasą CSS opona oraz nagłówek <h3>, <h4> (w HTML lub skrypcie)

    R.5.4 Skrypt 1 tworzy tyle bloków, ile wybiera zapytanie

    ok. 2 min
    Polecenie z arkusza

    Dla każdego zwróconego zapytaniem rekordu wyświetla blok z przypisaną klasą CSS opona, w którym znajdują się: obraz sezonu, nagłówek czwartego stopnia i nagłówek trzeciego stopnia.

    W bloku bocznym, nad odnośnikiem, pętla w składni szablonu: foreach (...): i endforeach; zamiast nawiasów klamrowych - HTML bloku zostaje zwykłym HTML-em z wcięciami. Jeden obieg = jeden blok, więc bloków jest tyle, ile wierszy zwróciło zapytanie (R.5.4).

    Blok to <article>: każda opona to samodzielna porcja treści (jak karta produktu), a tabela 2 arkusza opisuje article właśnie tak. Klasa opona z arkusza daje tło, kropkowaną ramkę i zaokrąglenie (R.2.8). Podgląd: dziesięć pustych bloków.

    opony.php PHP zmienione: 4 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->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>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  38. Krok 38: Skrypt 1: ikona sezonu

    Kryteria:

    R.5.6 Skrypt 1 wyświetla obraz lato.png, gdy opona jest letnia, zima.png , gdy jest zimowa, uniwer.png gdy jest uniwersalna

    ok. 3 min
    Polecenie z arkusza

    W przypadku opony letniej obraz lato.png, w przypadku zimowej obraz zima.png, w przypadku uniwersalnej obraz uniwer.png.

    Wybór ikony to osobna funkcja seasonIcon() z wyrażeniem match (PHP 8): wartość sezonu porównywana jest ściśle (===) z kolejnymi przypadkami i od razu zwracana jest nazwa pliku. default obsługuje sezon uniwersalny - i każdą inną wartość, więc nieznany sezon w bazie nie zostawi pustego obrazka (R.5.6).

    Zamiast trzech gałęzi if z trzema echo jest tabela „sezon → plik” w jednym miejscu, a w szablonie jedno <img>. Tekst alternatywny z nazwą sezonu przechodzi przez e().

    Typowe błędy

    match porównuje ściśle - 'Letnia' z wielkiej litery nie pasuje do 'letnia' i trafi do default. Wartości muszą być dokładnie takie jak w kolumnie sezon.

    opony.php PHP zmienione: 10 linii
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  39. Krok 39: Skrypt 1: nagłówek czwartego stopnia z producentem i modelem

    Kryteria: ,

    R.2.8 W bloku bocznym utworzony przynajmniej jeden blok z klasą CSS opona oraz nagłówek <h3>, <h4> (w HTML lub skrypcie)

    R.5.5 Skrypt 1 wyświetla przynajmniej dla jednego rekordu teksty „Opona: <producent> <model>” oraz „Cena: <cena>”

    ok. 1 min
    Polecenie z arkusza

    Nagłówek czwartego stopnia o treści „Opona: <producent> <model>”.

    <h4> ze stałym tekstem w HTML i dwiema wartościami wstawionymi przez <?= ... ?> (skrót od <?php echo ... ?>), każda przez e() (R.2.8, R.5.5). Kolumny po nazwach - kolejność w SELECT nie ma już znaczenia.

    opony.php PHP zmienione: 1 linia
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  40. Krok 40: Skrypt 1: nagłówek trzeciego stopnia z ceną

    Kryteria:

    R.5.5 Skrypt 1 wyświetla przynajmniej dla jednego rekordu teksty „Opona: <producent> <model>” oraz „Cena: <cena>”

    ok. 1 min
    Polecenie z arkusza

    Nagłówek trzeciego stopnia o treści „Cena: <cena>”.

    <h3> z ceną (R.5.5). Cena przychodzi z bazy jako napis „101.50” (kolumna decimal - PDO oddaje ją jako tekst, żeby nie zgubić groszy na zaokrągleniach) i tak zostaje wypisana, jak na ilustracji 2. Teraz działa reguła h3 z CSS, a blok wygląda jak na ilustracji; pasek przewijania bloku bocznego pojawia się sam.

    W podglądzie przy równych cenach (110,00 i 110,50) kolejność opon może być inna niż na zrzucie - zapytanie sortuje tylko po cenie, a remisy baza układa dowolnie.

    opony.php PHP zmienione: 1 linia
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  41. Krok 41: 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 - zapytanie 2 lub skrypt 3 - zapytanie 3

    ok. 2 min
    Polecenie z arkusza

    Skrypt 2 wysyła do bazy zapytanie 2.

    Zapytanie 2 jako zapytanie przygotowane: w miejscu numeru katalogowego stoi parametr nazwany :catalogNumber, a wartość 9 idzie osobno w execute([...]) (R.5.3). Dziś to stała, ale „opona dnia” jutro będzie inna - może przyjść z panelu administratora albo z adresu strony. Gdy wartość przychodzi z zewnątrz, oddzielenie jej od treści zapytania jest jedyną pewną ochroną przed SQL injection, więc tę formę warto mieć w palcach od razu.

    Wynik ma jeden wiersz, więc wystarcza fetch() - zwraca ten wiersz albo false, gdy opony o takim numerze nie ma.

    Typowe błędy

    fetchAll() przy jednym wierszu daje tablicę tablic - wtedy producent to $offer[0]['producent'], nie $offer['producent'].

    opony.php PHP zmienione: 4 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  42. Krok 42: Skrypt 2: nagłówek z producentem i modelem

    Kryteria:

    R.5.7 Skrypt 2 wyświetla zwrócony rekord w postaci: „<producent> model <model>”, „Sezon: <sezon>”, „Tylko <cena> zł!”

    ok. 1 min
    Polecenie z arkusza

    Wyświetla w nagłówku drugiego stopnia treść „<producent> model <model>”.

    <?php if ($offer): ?> … <?php endif; ?> - nagłówki z ofertą pojawią się tylko, gdy wiersz istnieje. Bez tego warunku usunięcie opony numer 9 z bazy kończyłoby się błędem „Trying to access array offset on value of type bool” w miejscu oferty. Podgląd: „uLFA model AL135” (R.5.7).

    opony.php PHP zmienione: 3 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  43. Krok 43: Skrypt 2: nagłówek z sezonem

    Kryteria:

    R.5.7 Skrypt 2 wyświetla zwrócony rekord w postaci: „<producent> model <model>”, „Sezon: <sezon>”, „Tylko <cena> zł!”

    ok. 1 min
    Polecenie z arkusza

    W kolejnym nagłówku drugiego stopnia treść „Sezon: <sezon>”.

    Drugi nagłówek wewnątrz tego samego warunku: „Sezon: zimowa” (R.5.7).

    opony.php PHP zmienione: 1 linia
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  44. Krok 44: Skrypt 2: nagłówek z ceną

    Kryteria:

    R.5.7 Skrypt 2 wyświetla zwrócony rekord w postaci: „<producent> model <model>”, „Sezon: <sezon>”, „Tylko <cena> zł!”

    ok. 1 min
    Polecenie z arkusza

    W kolejnym nagłówku drugiego stopnia treść „Tylko <cena> zł!”.

    Trzeci nagłówek: „Tylko 101.50 zł!”, jak na ilustracji 2 (R.5.7). Skrypt 2 jest kompletny.

    opony.php PHP zmienione: 1 linia
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
                <h2>Tylko <?= e($offer['cena']) ?> zł!</h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  45. Krok 45: Skrypt 3: zapytanie 3 do bazy

    Kryteria:

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

    ok. 2 min
    Polecenie z arkusza

    Skrypt 3 wysyła do bazy zapytanie 3.

    Zapytanie 3 z aliasami tabel, tak samo jak w kwerendy.txt (R.5.3). Napis z zapytaniem może mieć kilka linii - baza ignoruje znaki nowej linii, a kod czyta się jak w phpMyAdmin. Jeden losowy wiersz trafia do $latestOrder przez fetch().

    Strona odświeża się co 10 sekund, więc przy każdym odświeżeniu baza wylosuje inne zamówienie. Podgląd pokazuje zamówienie 7 z ilustracji 2.

    opony.php PHP zmienione: 8 linii
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    
        $latestOrder = $db->query(
            'SELECT z.id_zam, z.ilosc, o.model, o.cena
             FROM zamowienie AS z
             JOIN opony AS o ON o.nr_kat = z.nr_kat
             ORDER BY RAND()
             LIMIT 1'
        )->fetch();
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
                <h2>Tylko <?= e($offer['cena']) ?> zł!</h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  46. Krok 46: Skrypt 3: nagłówek z identyfikatorem, ilością i modelem

    Kryteria:

    R.5.9 Skrypt 3 wyświetla teksty „<id_zam> <ilosc> sztuki modelu <model>” lub „Wartość zamówienia <wartość> zł”, z wyliczoną wartością

    ok. 1 min
    Polecenie z arkusza

    Wyświetla w nagłówku drugiego stopnia treść „<id_zam> <ilosc> sztuki modelu <model>”.

    Ten sam wzór co w sekcji 1: warunek na istnienie wiersza (pusta tabela zamówień nie wywoła błędu) i nagłówek z trzema wartościami. Podgląd: „7 4 sztuki modelu AL100”, jak na ilustracji 2 (R.5.9).

    opony.php PHP zmienione: 3 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    
        $latestOrder = $db->query(
            'SELECT z.id_zam, z.ilosc, o.model, o.cena
             FROM zamowienie AS z
             JOIN opony AS o ON o.nr_kat = z.nr_kat
             ORDER BY RAND()
             LIMIT 1'
        )->fetch();
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
                <h2>Tylko <?= e($offer['cena']) ?> zł!</h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
    <?php if ($latestOrder): ?>
                <h2><?= e($latestOrder['id_zam']) ?> <?= e($latestOrder['ilosc']) ?> sztuki modelu <?= e($latestOrder['model']) ?></h2>
    <?php endif; ?>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  47. Krok 47: Skrypt 3: wartość zamówienia

    Kryteria:

    R.5.8 Skrypt 3 wylicza wartość zamówienia jako iloczyn ceny i ilości opon

    ok. 1 min
    Polecenie z arkusza

    Wartość oznacza wyliczoną wartość na podstawie zwróconych z bazy ilości i ceny.

    Wartość to iloczyn ilości i ceny policzony w PHP (R.5.8): $latestOrder['ilosc'] * $latestOrder['cena'], dla zamówienia z ilustracji 4 × 290,50 = 1162. Operator warunkowy ? : daje 0, gdy zamówienia nie ma - mnożenie false przez cokolwiek byłoby błędem.

    Liczenie stoi w części z logiką, nie w szablonie: szablon tylko wypisuje gotowy wynik.

    Typowe błędy

    Wyliczenie w SQL (SELECT ilosc * cena AS wartosc) daje ten sam wynik, ale arkusz każe liczyć „na podstawie zwróconych z bazy ilości i ceny”, a klucz R.5.8 sprawdza mnożenie w skrypcie.

    opony.php PHP zmienione: 2 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    
        $latestOrder = $db->query(
            'SELECT z.id_zam, z.ilosc, o.model, o.cena
             FROM zamowienie AS z
             JOIN opony AS o ON o.nr_kat = z.nr_kat
             ORDER BY RAND()
             LIMIT 1'
        )->fetch();
    
        $orderValue = $latestOrder ? $latestOrder['ilosc'] * $latestOrder['cena'] : 0;
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
                <h2>Tylko <?= e($offer['cena']) ?> zł!</h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
    <?php if ($latestOrder): ?>
                <h2><?= e($latestOrder['id_zam']) ?> <?= e($latestOrder['ilosc']) ?> sztuki modelu <?= e($latestOrder['model']) ?></h2>
    <?php endif; ?>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

  48. Krok 48: Skrypt 3: nagłówek z wartością zamówienia

    Kryteria:

    R.5.9 Skrypt 3 wyświetla teksty „<id_zam> <ilosc> sztuki modelu <model>” lub „Wartość zamówienia <wartość> zł”, z wyliczoną wartością

    ok. 1 min
    Polecenie z arkusza

    W kolejnym nagłówku drugiego stopnia treść „Wartość zamówienia <wartość> zł”.

    number_format($orderValue, 2, ',', ' ') zapisuje kwotę po polsku: dwa miejsca po przecinku, przecinek dziesiętny, spacja między tysiącami - „1 162,00 zł” zamiast „1162 zł”. Wartość jest ta sama, zmienia się tylko zapis (R.5.9). Wynik number_format to cyfry, spacje i przecinek, więc nie wymaga e().

    Ceny opon w nagłówkach zostają w zapisie z bazy, jak na ilustracji 2 - formatowana jest tylko kwota wyliczona przez skrypt.

    opony.php PHP zmienione: 1 linia
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    
        $latestOrder = $db->query(
            'SELECT z.id_zam, z.ilosc, o.model, o.cena
             FROM zamowienie AS z
             JOIN opony AS o ON o.nr_kat = z.nr_kat
             ORDER BY RAND()
             LIMIT 1'
        )->fetch();
    
        $orderValue = $latestOrder ? $latestOrder['ilosc'] * $latestOrder['cena'] : 0;
    } 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
                <h2>Tylko <?= e($offer['cena']) ?> zł!</h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
    <?php if ($latestOrder): ?>
                <h2><?= e($latestOrder['id_zam']) ?> <?= e($latestOrder['ilosc']) ?> sztuki modelu <?= e($latestOrder['model']) ?></h2>
                <h2>Wartość zamówienia <?= number_format($orderValue, 2, ',', ' ') ?> zł</h2>
    <?php endif; ?>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (opony.php)
  49. Krok 49: Zamknięcie połączenia z bazą

    Kryteria:

    R.5.2 Skrypt realizuje połączenie z serwerem bazy danych i wybór bazy opony oraz 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 - to ostatnia operacja na bazie. Szablon 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”.

    opony.php PHP zmienione: 2 linie
    <?php
    header('refresh: 10');
    
    $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,
        ]);
    
        $cheapestTyres = $db->query(
            'SELECT nr_kat, producent, model, sezon, cena FROM opony ORDER BY cena LIMIT 10'
        )->fetchAll();
    
        $offerQuery = $db->prepare('SELECT producent, model, sezon, cena FROM opony WHERE nr_kat = :catalogNumber');
        $offerQuery->execute(['catalogNumber' => 9]);
        $offer = $offerQuery->fetch();
    
        $latestOrder = $db->query(
            'SELECT z.id_zam, z.ilosc, o.model, o.cena
             FROM zamowienie AS z
             JOIN opony AS o ON o.nr_kat = z.nr_kat
             ORDER BY RAND()
             LIMIT 1'
        )->fetch();
    
        $orderValue = $latestOrder ? $latestOrder['ilosc'] * $latestOrder['cena'] : 0;
    
        $db = 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');
    }
    
    function seasonIcon(string $season): string
    {
        return match ($season) {
            'letnia' => 'lato.png',
            'zimowa' => 'zima.png',
            default => 'uniwer.png',
        };
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>OPONY</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <main>
            <aside>
    <?php foreach ($cheapestTyres as $tyre): ?>
                <article class="opona">
                    <img src="<?= seasonIcon($tyre['sezon']) ?>" alt="<?= e($tyre['sezon']) ?>">
                    <h4>Opona: <?= e($tyre['producent']) ?> <?= e($tyre['model']) ?></h4>
                    <h3>Cena: <?= e($tyre['cena']) ?></h3>
                </article>
    <?php endforeach; ?>
                <p><a href="https://opona.pl/">więcej ofert</a></p>
            </aside>
            <section class="offer">
                <img src="opona.png" alt="Opona">
                <h2>Opona dnia</h2>
    <?php if ($offer): ?>
                <h2><?= e($offer['producent']) ?> model <?= e($offer['model']) ?></h2>
                <h2>Sezon: <?= e($offer['sezon']) ?></h2>
                <h2>Tylko <?= e($offer['cena']) ?> zł!</h2>
    <?php endif; ?>
            </section>
            <section class="order">
                <h2>Najnowsze zamówienie</h2>
    <?php if ($latestOrder): ?>
                <h2><?= e($latestOrder['id_zam']) ?> <?= e($latestOrder['ilosc']) ?> sztuki modelu <?= e($latestOrder['model']) ?></h2>
                <h2>Wartość zamówienia <?= number_format($orderValue, 2, ',', ' ') ?> zł</h2>
    <?php endif; ?>
            </section>
        </main>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

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

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

    Krok 50 · po tym etapie masz 37 z 37 kryteriów (pkt)

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

    ok. 2 min
    Polecenie z arkusza

    Czas przeznaczony na wykonanie zadania wynosi 150 minut.

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

    wersja podstawowawersja nowoczesna
    układfloat: left na blokach i clear: both w stopcesiatka: grid-template-columns i grid-template-areas
    kolorywpisane w każdej regulezmienne w :root
    blok opony<div><article> z display: flow-root
    bazafunkcje mysqli_* z tabeli 1PDO, wyjątki, config.php
    wybór ikonyif / elseif / elsefunkcja z match
    wypisywanieecho w pętli whilelogika na górze, szablon z foreach/endforeach i e()
    pusta tabelabłąd PHPwarunek if ($offer) / if ($latestOrder)

    float został w jednym miejscu - przy obrazie, bo opływanie tekstem to jego właściwe zadanie i klucz R.4.8 go wymaga.

    Na egzaminie: wersja podstawowa 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 składni, a literówka w PDO::ATTR_ERRMODE to błąd, którego nikt nie podpowie. Formularza tu nie ma, więc nie ma też potrzeby walidacji wejścia ani tokenu CSRF - pojawią się w arkuszach, w których strona zapisuje dane.

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

  51. Etap 6: Oddanie pracy

    Krok 51 · po tym etapie masz 37 z 37 kryteriów (pkt)

    Krok 51: 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: import.png, kw1.png … kw4.png, kwerendy.txt, lato.png, opona.png, opony.php, config.php (bez niego strona się nie połączy), przeglądarka.txt, styl.css, uniwer.png, zima.png. 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 48.

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.