Przejdź do głównej treści
EE.09-01-22.06-SGCzerwiec 2022 · 45 kroków · ok. 87 min · 42 kryteria z klucza

Rozwiązanie krok po kroku: Forum miłośników psów (Nowoczesny kod (grid, PDO))

To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza forumpsy z czterema zapytaniami, avatar z przezroczystym tłem 100 × 100 px i strona index.php forum o psach z filmem, formularzem i listą odpowiedzi - i tak samo spełnia każde kryterium klucza. Inna jest droga:

  • siatka CSS zamiast float: oba bloki stoją w znaczniku <main>, a ich szerokości 40% i 60% opisuje grid-template-columns - stopka nie potrzebuje clear,
  • zmienne CSS dla trzech kolorów z arkusza - każdy jest wpisany raz,
  • PHP na PDO na górze pliku index.php, przed HTML-em: najpierw zapis nowej odpowiedzi, potem odczyt pytania i listy. W HTML-u zostają tylko pętla i wypisanie danych,
  • każda wartość z bazy przechodzi przez htmlspecialchars, odpowiedź jest przycinana z białych znaków, a zapis idzie zapytaniem przygotowanym - apostrof w odpowiedzi ani wpisany skrypt niczego nie psują,
  • po dodaniu odpowiedzi przekierowanie - odświeżenie strony nie doda tej samej odpowiedzi drugi raz.

To arkusz EE.09 z czerwca 2022 - kwalifikacji formuły 2017, którą zastąpiła INF.03. Baza, avatar i film pochodzą z oryginalnego archiwum CKE zad1.zip (załącznik na stronie arkusza), zapytania i zrzuty są policzone na MariaDB 10.4, a podgląd od kroków z PHP to wynik prawdziwego uruchomienia kodu w PHP 8.2 (klucz każe sprawdzać w XAMPP 8.1.2 - użyte konstrukcje działają w obu wersjach).

Jak liczą się punkty. Klucz ma 42 kryteria (R.1.1 - R.5.8), każde za 1 punkt. Próg zdania części praktycznej to 75%, czyli 32 punkty. Przy każdym kroku jest napisane, które kryteria spełnia; klucz przyjmuje zarówno mysqli, jak i PDO.

  1. Etap 1: Baza danych

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

    Krok 1: Folder zdającego i archiwum zad1.zip

    ok. 2 min
    Polecenie z arkusza

    Na pulpicie znajduje się archiwum o nazwie zad1.zip zabezpieczone hasłem: 3&PsY#@ Archiwum należy rozpakować. Na pulpicie konta Egzamin należy utworzyć folder. Jako nazwy folderu należy użyć numeru zdającego, którym został podpisany arkusz. Rozpakowane pliki należy umieścić w tym folderze.

    Folder na pulpicie nazwany numerem PESEL (w tym rozwiązaniu w jego miejscu stoi Bartosz Bryniarski), do niego archiwum zad1.zip rozpakowane z hasłem _3&PsY#@_. W środku: baza.sql, Avatar.jpg i video.mp4. Do tego folderu trafiają wszystkie rezultaty pracy, a stronę uruchamiasz z kopii w C:\xampp\htdocs.

  2. Krok 2: XAMPP i pusta baza forumpsy

    ok. 2 min
    Polecenie z arkusza

    Za pomocą narzędzia phpMyAdmin należy wykonać operacje na bazie danych: Utworzenie bazy danych o nazwie: forumpsy z zestawem polskich znaków (np. utf8_unicode_ci)

    Start przy Apache i MySQL, potem http://localhost/phpmyadmin, Nowa, nazwa forumpsy, porównywanie utf8_unicode_ci, Utwórz.

  3. Krok 3: Import pliku baza.sql i zrzut import.jpg

    Kryteria:

    R.1.1 Wykonano import tabel do bazy danych forumpsy , czynność udokumentowano plikiem o nazwie import w formacie JPEG

    ok. 3 min
    Polecenie z arkusza

    Import tabel do bazy forumpsy z pliku baza.sql. Czynność należy udokumentować zrzutem ekranu, na którym powinny być widoczne elementy wskazujące na poprawnie wykonany import tabel. Zrzut należy zapisać w formacie JPEG, pod nazwą import. Powinien on obejmować cały ekran monitora, z widocznym paskiem zadań.

    Baza forumpsy, karta Import, plik baza.sql, Wykonaj. Po imporcie są trzy tabele: konta (10 rekordów), odpowiedzi (7) i pytania (4). Zrzut całego ekranu z komunikatem o sukcesie i listą tabel - import.jpg (JPEG, w odróżnieniu od zrzutów zapytań).

    Podgląd strony po tym kroku
    Podgląd strony po kroku 3
  4. Krok 4: Zapytanie 1: nowa odpowiedź na pytanie 1

    Kryteria: ,

    R.1.2 Zapisano plik tekstowy o nazwie kwerendy zawierający co najmniej jedno zapytanie SQL, wynikające z treści zadania

    R.1.3 Utworzono zapytanie 1 wstawiające do tabeli odpowiedzi wiersz, o treści „Przyda Ci się także wysoki stół, na którym postawisz pieska.”, pytania_id = 1, konta_id = 5. W pliku z kwerendami lub na zrzucie istnieje zapis np.: INSERT INTO `odpowiedzi` (`Pytania_id`, `konta_id`, `odpowiedz`) VALUES (1, 5, 'Przyda Ci się także wysoki stół, na którym postawisz pieska.'); możliwe inne formy zapytania INSERT INTO oraz wynik działania zapytania udokumentowano zrzutem, na którym widoczny jest komunikat o poprawnie wykonanym zapytaniu lub tabela ze wstawionym rekordem

    ok. 3 min
    Polecenie z arkusza

    Zapytania SQL na bazie forumpsy zapisane do pliku kwerendy.txt. Zapytanie 1: wstawiające do tabeli odpowiedzi wiersz, o treści „Przyda Ci się także wysoki stół, na którym postawisz pieska.”. Jest on odpowiedzią na pytanie o id równym 1, udzieloną przez użytkownika o id równym 5.

    Lista kolumn bez id - numer nada AUTO_INCREMENT (nowa odpowiedź dostaje 8), potem wartości w tej samej kolejności: pytanie 1, użytkownik 5, treść w apostrofach. Zapytanie trafia do kwerendy.txt (R.1.2), zrzut - do kw1.png. Skrypt wykona to samo zapytanie z parametrem w miejscu treści.

    kwerendy.txt Tekst nowy plik
    -- Zapytanie 1
    INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz)
    VALUES (1, 5, 'Przyda Ci się także wysoki stół, na którym postawisz pieska.');
    Podgląd strony po tym kroku
    Podgląd strony po kroku 4
  5. Krok 5: Zapytanie 2: ile lat na forum

    Kryteria:

    R.1.4 Utworzono zapytanie 2 wybierające z tabeli konta jedynie pole nick oraz liczące ile lat dany użytkownik jest zarejestrowany na forum – alias latNaForum, dla rekordów, których nick jest równy Jacek, Tomek, Monika. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nick, YEAR(CURRENT_DATE())-rejestracja AS latNaForum FROM konta WHERE nick IN ('Jacek', 'Tomek', 'Monika'); (możliwe inne funkcje zamiast CURRENT_DATE()aktualny rok zwracany funkcją lub warunek z OR) oraz wynik działania zapytania udokumentowano zrzutem, na którym widoczne są jedynie 3 rekordy, jedynie pola nick i latNaForum wartości w kolumnie latNaForum są obliczone dla Jacka 3, Tomka 6, Moniki 2

    ok. 3 min
    Polecenie z arkusza

    Zapytanie 2: z tabeli konta dla rekordów, których nick to Jacek, Tomek, Monika wybierające jedynie pole nick oraz liczące ile lat dany użytkownik jest zarejestrowany na forum – kolumnę należy nazwać (alias) latNaForum. Zapytanie jest uniwersalne, gdy zmieni się aktualny rok, też powinno być liczone poprawnie

    YEAR(CURDATE()) - rok z dzisiejszej daty, więc zapytanie nie ma roku wpisanego na sztywno; od niego odejmujesz rok rejestracji i nazywasz wynik aliasem latNaForum. IN (...) zastępuje trzy warunki z OR.

    Wynik w 2026 roku: Jacek 7, Tomek 10, Monika 6. Zrzut: kw2.png.

    ⚠️ Rozbieżność arkusz-klucz: z danych w 2026 roku wychodzi 7, 10 i 6, a klucz CKE podaje 3, 6 i 2 - wynik z czerwca 2022, roku egzaminu. Klucz sam wymaga zapytania uniwersalnego, więc punkt daje zapytanie z YEAR(CURDATE()), a liczby zależą od roku, w którym się je wykonuje.

    kwerendy.txt Tekst zmienione: 5 linii
    -- Zapytanie 1
    INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz)
    VALUES (1, 5, 'Przyda Ci się także wysoki stół, na którym postawisz pieska.');
    
    -- Zapytanie 2
    SELECT nick, YEAR(CURDATE()) - rejestracja AS latNaForum
    FROM konta
    WHERE nick IN ('Jacek', 'Tomek', 'Monika');
    Podgląd strony po tym kroku
    Podgląd strony po kroku 5
  6. Krok 6: Zapytanie 3: autor pytania nr 1

    Kryteria:

    R.1.5 Utworzono zapytanie 3 wybierające jedynie nick i liczbę postów użytkownika z tabeli konta oraz odpowiadające im pole pytanie z tabeli pytania dla pytania, którego id jest równe 1. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = konta_id WHERE pytania.id = 1; (lub bez JOIN i porównanie kluczy po WHERE) oraz wynik działania zapytania udokumentowano zrzutem, na którym widoczny jest dokładnie jeden rekord o danych Jacek, 50, "Mam pytanie w jaki sposób dbać ..."

    ok. 3 min
    Polecenie z arkusza

    Zapytanie 3: wybierające jedynie nick i liczbę postów użytkownika z tabeli konta oraz odpowiadające im pole pytanie z tabeli pytania dla pytania, którego id jest równe 1. Należy posłużyć się relacją.

    JOIN pytania ON konta.id = pytania.konta_id - relacja z obrazu 1: każde pytanie z kontem autora. WHERE pytania.id = 1 z nazwą tabeli, bo kolumnę id mają obie. Wynik: Jacek, 50 i pytanie o sierść sznaucera. Każda klauzula w osobnej linii - przy złączeniach łatwiej wtedy czytać zapytanie. Zrzut: kw3.png.

    kwerendy.txt Tekst zmienione: 6 linii
    -- Zapytanie 1
    INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz)
    VALUES (1, 5, 'Przyda Ci się także wysoki stół, na którym postawisz pieska.');
    
    -- Zapytanie 2
    SELECT nick, YEAR(CURDATE()) - rejestracja AS latNaForum
    FROM konta
    WHERE nick IN ('Jacek', 'Tomek', 'Monika');
    
    -- Zapytanie 3
    SELECT nick, postow, pytanie
    FROM konta
    JOIN pytania ON konta.id = pytania.konta_id
    WHERE pytania.id = 1;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 6
  7. Krok 7: Zapytanie 4: odpowiedzi na pytanie nr 1

    Kryteria:

    R.1.6 Utworzono zapytanie 4 wybierające jedynie id odpowiedzi oraz odpowiedź z tabeli odpowiedzi oraz odpowiadające im pole nick z tabeli konta dla id pytania równego 1. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT odpowiedzi.id, odpowiedz, nick FROM odpowiedzi JOIN konta ON konta_id = konta.id WHERE Pytania_id = 1; (lub bez JOIN i porównanie kluczy po WHERE) oraz wynik działania zapytania udokumentowano zrzutem, na którym widać tylko rekordy z id równym 1, 2, 3; pola id, odpowiedz i nick lub dodatkowo inne rekordy powstałe wyłącznie na skutek działania zapytania 1

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 4: wybierające jedynie id odpowiedzi oraz odpowiedź z tabeli odpowiedzi oraz odpowiadające im pole nick z tabeli konta dla id pytania równego 1. Należy posłużyć się relacją.

    Złączenie odpowiedzi z kontami ich autorów i warunek odpowiedzi.Pytania_id = 1. Wynik: odpowiedzi 1, 2, 3 (Anna, Gosia, Tomek) i 8 (Gosia) - tę ostatnią dodało zapytanie 1, a klucz przyjmuje ją wprost. Zrzut: kw4.png.

    kwerendy.txt Tekst zmienione: 6 linii
    -- Zapytanie 1
    INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz)
    VALUES (1, 5, 'Przyda Ci się także wysoki stół, na którym postawisz pieska.');
    
    -- Zapytanie 2
    SELECT nick, YEAR(CURDATE()) - rejestracja AS latNaForum
    FROM konta
    WHERE nick IN ('Jacek', 'Tomek', 'Monika');
    
    -- Zapytanie 3
    SELECT nick, postow, pytanie
    FROM konta
    JOIN pytania ON konta.id = pytania.konta_id
    WHERE pytania.id = 1;
    
    -- Zapytanie 4
    SELECT odpowiedzi.id, odpowiedz, nick
    FROM odpowiedzi
    JOIN konta ON odpowiedzi.konta_id = konta.id
    WHERE odpowiedzi.Pytania_id = 1;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 7
  8. Etap 2: Grafika i strona HTML

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

    Krok 8: Avatar: przezroczyste tło

    ok. 3 min
    Polecenie z arkusza

    Cechy grafiki z pliku Avatar: Tło przezroczyste

    GIMP: Warstwa → Przezroczystość → Dodaj kanał alfa, potem Zaznaczanie rozmytego obszaru kliknięte w białe tło i Delete. Różdżka zaznacza tylko obszar połączony z klikniętym punktem, więc białe odblaski w oczach zostają - Kolor na alfę zrobiłby je przezroczystymi.

    Podgląd strony po tym kroku
    Podgląd strony po kroku 8
  9. Krok 9: Avatar: 100 × 100 px w formacie PNG

    Kryteria:

    R.2.1 Obraz w pliku Avatar ma wymiary 100 px na 100 px, tło przezroczyste i jest zapisany w formacie obsługującym przezroczystość

    ok. 2 min
    Polecenie z arkusza

    Rozmiary 100 px na 100 px. Grafika zapisana pod nazwą Avatar w formacie obsługującym przezroczystość

    Obraz → Skaluj obraz z 640 × 640 do 100 × 100 px, potem Eksportuj jako Avatar.png - PNG zachowuje przezroczystość, JPEG nie (R.2.1).

    Podgląd strony po tym kroku
    Podgląd strony po kroku 9
  10. Krok 10: Plik index.php z deklaracją HTML5

    Kryteria:

    R.2.2 Witrynę internetową zapisano w pliku o nazwie index.php

    ok. 1 min
    Polecenie z arkusza

    Cechy witryny: Składa się ze strony o nazwie index.php

    Plik index.php w folderze zdającego, od razu z deklaracją <!DOCTYPE html> w pierwszej linii (arkusz wymaga HTML5) i szkieletem <html>, <head>, <body>. Kod PHP dojdzie na samą górę pliku, przed deklarację.

    index.php PHP nowy plik
    <!DOCTYPE html>
    <html>
    <head>
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  11. Krok 11: Kodowanie 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>, a plik zapisany w UTF-8 bez BOM - znak BOM przed <?php byłby „wypisany” przed nagłówkiem przekierowania i header() przestałby działać.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  12. Krok 12: Język strony: polski

    Kryteria:

    R.2.3 Zapisano jawnie standard kodowania polskich znaków i deklarację HTML5 <!DOCTYPE HTML> oraz zadeklarowano dla witryny język polski (np. lang = 'pl')

    ok. 1 min
    Polecenie z arkusza

    Zadeklarowany język zawartości witryny: polski

    lang="pl" w znaczniku <html> - deklaracja HTML5, kodowanie i język są gotowe (R.2.3).

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

    Kryteria:

    R.2.4 Nadano tytuł strony: Forum o psach

    ok. 1 min
    Polecenie z arkusza

    Tytuł strony widoczny na karcie przeglądarki: „Forum o psach”

    <title> z tytułem z arkusza (R.2.4). Obok <meta name="viewport"> - bez niej telefon pokazuje stronę jak pomniejszony ekran komputera. Arkusz tego nie wymaga, ale to stały element każdej współczesnej strony.

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

    Kryteria:

    R.3.8 Strona zawiera działające połączenie z zewnętrznym arkuszem stylów o nazwie styl.css , formatowanie pochodzi jedynie z tego arkusza Uwaga: kryterium to nie jest spełnione również, gdy zdający źle przypisał co najmniej jeden selektor do bloku (np. kropka zamiast #)

    ok. 1 min
    Polecenie z arkusza

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

    <link rel="stylesheet" href="styl.css"> - całe formatowanie strony przyjdzie z tego pliku (R.3.8).

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  15. Krok 15: Bloki: baner, lewy, prawy i stopka

    Kryteria:

    R.2.5 Wygląd strony zdefiniowano dzieląc ją na bloki: baner, lewy, prawy, stopka. Zastosowano znaczniki sekcji

    ok. 2 min
    Polecenie z arkusza

    Podział strony na: blok banera, obok siebie blok lewy i prawy, stopka. Podział zrealizowany za pomocą znaczników sekcji

    <header>, dwie sekcje i <footer> (R.2.5). Sekcje są opakowane w <main> - znacznik głównej treści strony, który w CSS stanie się siatką z dwiema kolumnami. Opakowanie ma sens także bez stylów: czytnik ekranu pozwala przeskoczyć prosto do <main>, z pominięciem banera.

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

    ok. 1 min
    Polecenie z arkusza

    Zawartość banera: nagłówek pierwszego stopnia o treści „Forum miłośników psów”

    <h1> w banerze - jedyny nagłówek pierwszego stopnia na stronie.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
            </section>
            <section id="right">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  17. Krok 17: Blok lewy: avatar z tekstem alternatywnym

    Kryteria:

    R.2.8 W bloku lewym umieszczono obraz Avatar z tekstem alternatywnym "Użytkownik forum"

    ok. 1 min
    Polecenie z arkusza

    Zawartość bloku lewego: Obraz o nazwie Avatar i tekście alternatywnym „Użytkownik forum”

    <img> z plikiem Avatar.png i tekstem alternatywnym (R.2.8). Atrybuty width i height z prawdziwymi wymiarami pliku pozwalają przeglądarce zarezerwować miejsce, zanim obraz się wczyta - tekst pod nim nie „skacze” w dół.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
            </section>
            <section id="right">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  18. Krok 18: Blok lewy: film w pętli z przyciskami

    Kryteria: , ,

    R.2.10 W bloku lewym umieszczono film video.mp4 (np. znaczniki video i source lub jedynie video z atrybutem src lub znacznik object lub embed)

    R.3.5 Video odtwarza się w pętli

    R.3.6 Na filmie widoczne są przyciski sterujące

    ok. 2 min
    Polecenie z arkusza

    Film o nazwie video.mp4 z przyciskami do nawigacji, odtwarzający się w pętli

    <video> z atrybutami logicznymi controls (przyciski - R.3.6) i loop (odtwarzanie w pętli - R.3.5). Między avatarem a filmem PHP wstawi dane autora pytania.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  19. Krok 19: Blok prawy: formularz wysyłany metodą POST

    Kryteria:

    R.3.3 Zdefiniowano wysyłanie danych z formularza do tego samego pliku metodą POST (poprawne również bez atrybutu action)

    ok. 1 min
    Polecenie z arkusza

    Zawartość bloku prawego: Formularz wysyłający dane metodą bezpieczną do tego samego pliku

    <form method="post"> bez atrybutu action - dane wracają do index.php (R.3.3). POST, bo odpowiedź nie powinna trafiać do adresu strony ani historii przeglądarki.

    index.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  20. Krok 20: Pole wielowierszowe 4 × 40

    ok. 2 min
    Polecenie z arkusza

    Pole wieloliniowe o 4 wierszach i 40 kolumnach

    <textarea> z rows="4" i cols="40", nazwa pola odpowiedz - pod tym kluczem PHP odczyta treść. Dwa dodatki:

    • maxlength="1000" - przeglądarka nie pozwoli wpisać dłuższej odpowiedzi; serwer i tak sprawdzi to sam, bo formularz da się ominąć,
    • aria-label - pole nie ma widocznej etykiety (obraz 2 jej nie pokazuje), więc czytnik ekranu dostaje nazwę pola z tego atrybutu zamiast samego „pole edycyjne”.
    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  21. Krok 21: Przycisk „Dodaj odpowiedź”

    Kryteria: ,

    R.2.9 W bloku prawym umieszczono formularz z polem wielowierszowym (<textarea>) i przyciskiem, zastosowano znacznik <form>

    R.3.4 Pole wielowierszowe ma 4 wiersze (rows) i 40 kolumn (cols) oraz przycisk wysyła dane z formularza (input lub button z typem submit lub button bez typu)

    ok. 1 min
    Polecenie z arkusza

    Przycisk o treści „Dodaj odpowiedź”, którego wciśnięcie powoduje wysłanie danych do skryptu 2

    <button type="submit"> wysyła formularz. Formularz, pole i przycisk (R.2.9) oraz 4 wiersze, 40 kolumn i wysyłanie (R.3.4) są gotowe.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  22. Krok 22: Nagłówek „Odpowiedzi na pytanie”

    ok. 1 min
    Polecenie z arkusza

    Nagłówek drugiego stopnia o treści „Odpowiedzi na pytanie”

    <h2> pod formularzem.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
                <h2>Odpowiedzi na pytanie</h2>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  23. Krok 23: Lista numerowana na odpowiedzi

    ok. 1 min
    Polecenie z arkusza

    Lista numerowana (uporządkowana), której elementy generowane są skryptem 3

    Pusta <ol> - elementy wypisze pętla PHP.

    index.php PHP zmienione: 2 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  24. Krok 24: Stopka: autor

    ok. 1 min
    Polecenie z arkusza

    Zawartość stopki: Tekst o treści „Autor: ”, dalej wstawiony numer zdającego

    Na egzaminie po dwukropku wpisujesz swój numer PESEL; tu stoi Bartosz Bryniarski. Tekst w paragrafie <p> - w nim zmieści się też odnośnik.

    index.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
            <p>Autor: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  25. Krok 25: Stopka: odnośnik otwierany w nowej karcie

    Kryteria:

    R.3.2 Na stronie umieszczono odnośnik, którego kliknięcie inicjuje otwarcie strony http://mojestrony.pl/ w nowej karcie przeglądarki

    ok. 1 min
    Polecenie z arkusza

    Za numerem zdającego wstawiony odnośnik do strony http://mojestrony.pl/ otwierający się w osobnej karcie przeglądarki i podpisany „Zobacz nasze realizacje”

    Pełny adres z http:// i target="_blank" (R.3.2). Dodatek rel="noopener": otwarta w nowej karcie strona nie dostaje dostępu do karty forum (obiektu window.opener), więc nie może jej po cichu podmienić na fałszywą. Nowe przeglądarki robią to już domyślnie, ale atrybut zostaje dla starszych.

    index.php PHP zmienione: 4 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  26. Etap 3: Styl CSS

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

    Krok 26: Plik styl.css: kolory jako zmienne i krój Garamond

    Kryteria:

    R.4.1 Ustawiono domyślne formatowanie wszystkich selektorów (*): krój czcionki: Garamond, serif (czcionka Garamond jako pierwsza)

    ok. 2 min
    Polecenie z arkusza

    Styl CSS zdefiniowany w całości w zewnętrznym pliku o nazwie styl.css. Ustawione domyślne wartości dla wszystkich selektorów stylu CSS: krój czcionki Garamond, w przypadku jego braku – serif

    Na początku pliku zmienne CSS w :root: trzy kolory z arkusza dostają nazwy --green-dark, --green-light i --text-dark. Kolor #97b498 występuje w arkuszu pięć razy (dwa gradienty, przycisk, :hover), więc zmiana odcienia to jedna linia zamiast pięciu.

    Selektor * z krojem Garamond, serif (R.4.1) - obejmuje też pole tekstowe i przycisk, które kroju po rodzicu nie dziedziczą.

    styl.css CSS nowy plik
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    Podgląd strony po tym kroku (index.php)
  27. Krok 27: Baner: gradient, biały tekst, wyśrodkowanie, marginesy wewnętrzne

    Kryteria:

    R.4.7 Ustawiono wyrównanie tekstu do środka dla banera

    ok. 2 min
    Polecenie z arkusza

    Dla bloku banera: tło będące pionowym gradientem liniowym od koloru #97b498 do #c8e6c9, biały kolor czcionki, wyrównanie tekstu do środka, marginesy wewnętrzne 5 px

    linear-gradient bez kierunku biegnie z góry na dół, czyli pionowo; zmienne działają także wewnątrz funkcji. Wyśrodkowanie (R.4.7), biały tekst i padding: 5px.

    styl.css CSS zmienione: 7 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    Podgląd strony po tym kroku (index.php)
  28. Krok 28: Blok lewy: tło, kolor tekstu i wysokość

    ok. 1 min
    Polecenie z arkusza

    Dla bloku lewego: kolor tła #c8e6c9, kolor czcionki #005005, szerokość 40%, wysokość 600 px

    Tło, kolor tekstu i wysokość na samym bloku. Szerokość 40% to szerokość kolumny siatki - ustawi ją reguła main.

    styl.css CSS zmienione: 6 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    Podgląd strony po tym kroku (index.php)
  29. Krok 29: Blok prawy: kolor tekstu, wysokość i paski przewijania

    Kryteria: ,

    R.4.5 Ustawiono wysokość 600px dla bloków lewego i prawego

    R.4.9 Ustawiono zawsze widoczne paski przewijana dla bloku prawego np. overflow: scroll;

    ok. 1 min
    Polecenie z arkusza

    Dla bloku prawego: kolor czcionki #005005, szerokość 60%, wysokość 600 px, zawsze widoczne paski przewijana

    overflow: scroll - paski przewijania widoczne zawsze, nawet przy krótkiej liście (R.4.9). Oba bloki mają po 600 px (R.4.5); szerokość 60% - w siatce.

    styl.css CSS zmienione: 6 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    Podgląd strony po tym kroku (index.php)
  30. Krok 30: Stopka: gradient odwrotny, biały tekst, marginesy wewnętrzne

    Kryteria:

    R.4.3 Ustawiono gradient liniowy pionowy od koloru #97b498 do #c8e6c9 dla banera np. background: linear-gradient(#97b498, #c8e6c9) i odwrotnej kolejności dla stopki np. background: linear-gradient(#c8e6c9, #97b498)

    ok. 1 min
    Polecenie z arkusza

    Dla stopki: tło będące pionowym gradientem liniowym od koloru #c8e6c9 do #97b498, biały kolor czcionki, marginesy wewnętrzne 12 px

    Te same zmienne w odwrotnej kolejności. Gradienty banera i stopki są gotowe (R.4.3). Druga reguła zeruje domyślny margines paragrafu w stopce - bez niej stopka byłaby wyraźnie wyższa niż na obrazie 2.

    styl.css CSS zmienione: 10 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    
    footer {
        background: linear-gradient(var(--green-light), var(--green-dark));
        color: white;
        padding: 12px;
    }
    
    footer p {
        margin: 0;
    }
    Podgląd strony po tym kroku (index.php)
  31. Krok 31: Układ bloków: siatka z kolumnami 40% i 60%

    Kryteria:

    R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z Obrazem 2 w arkuszu egzaminacyjnym (prawidłowo zastosowano właściwości CSS układające bloki na stronie, np. float i clear albo display flex albo grid). Nie należy brać pod uwagę umiejscowienia filmu

    ok. 3 min
    Polecenie z arkusza

    Podział zrealizowany za pomocą znaczników sekcji tak, aby wygląd układu bloków strony po uruchomieniu w przeglądarce, był zgodny z Obrazem 2

    <main> staje się siatką: display: grid i grid-template-columns: 40% 60% - dwie kolumny o szerokościach z arkusza, a każda sekcja zajmuje kolejną komórkę. Bloki stoją obok siebie bez float, a stopka po prostu stoi pod <main>, bez clear (R.3.1).

    Szerokość bloku to szerokość jego kolumny - klucz przyjmuje grid-template-columns jako ustawienie 40% i 60% (R.4.6, razem z filmem w kroku niżej). Kolumny w procentach liczą się od szerokości <main>, a bloki nie mają marginesów, więc suma 100% mieści się dokładnie.

    Typowe błędy

    Odstęp gap między kolumnami przy 40% + 60% wypycha siatkę poza ekran (pojawia się poziomy pasek przewijania strony) - przy procentach bez odstępu albo kolumny 2fr 3fr, które same odejmą gap.

    styl.css CSS zmienione: 5 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    
    footer {
        background: linear-gradient(var(--green-light), var(--green-dark));
        color: white;
        padding: 12px;
    }
    
    footer p {
        margin: 0;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
    }
    Podgląd strony po tym kroku (index.php)
  32. Krok 32: Film: szerokość 100%

    Kryteria:

    R.4.6 Ustawiono szerokość 40% dla bloku lewego i 60% dla prawego (width lub grid- template-columns lub flex) oraz 100% dla selektora osadzającego film (np. video, object, embed)

    ok. 1 min
    Polecenie z arkusza

    Dla selektora filmu: szerokość 100%

    Selektor znacznika video (klucz nie przyjmuje klasy) - film wypełnia kolumnę bloku lewego. Szerokości bloków z siatki i filmu dają R.4.6.

    styl.css CSS zmienione: 4 linie
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    
    footer {
        background: linear-gradient(var(--green-light), var(--green-dark));
        color: white;
        padding: 12px;
    }
    
    footer p {
        margin: 0;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
    }
    
    video {
        width: 100%;
    }
    Podgląd strony po tym kroku (index.php)
  33. Krok 33: Formularz: marginesy wewnętrzne

    Kryteria:

    R.4.8 Ustawiono marginesy wewnętrzne (padding) 5px dla banera, 12px dla stopki, 10px (górny i dolny) 50px (lewy i prawy) dla selektora form

    ok. 1 min
    Polecenie z arkusza

    Dla selektora formularza: marginesy wewnętrzne górny i dolny 10 px, lewy i prawy 50 px

    padding: 10px 50px - pion, potem poziom. Marginesy wewnętrzne banera, stopki i formularza są gotowe (R.4.8).

    styl.css CSS zmienione: 4 linie
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    
    footer {
        background: linear-gradient(var(--green-light), var(--green-dark));
        color: white;
        padding: 12px;
    }
    
    footer p {
        margin: 0;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
    }
    
    video {
        width: 100%;
    }
    
    form {
        padding: 10px 50px;
    }
    Podgląd strony po tym kroku (index.php)
  34. Krok 34: Przycisk: tło i biały tekst

    Kryteria: ,

    R.4.2 Ustawiono kolor tła: #c8e6c9 dla bloku lewego i #97b498 dla przycisku

    R.4.4 Ustawiono biały kolor czcionki dla bloku banera, stopki i przycisku oraz #005005 dla bloków lewego i prawego

    ok. 1 min
    Polecenie z arkusza

    Dla przycisku: kolor tła #97b498, biały kolor czcionki

    Kolory tła bloku lewego i przycisku (R.4.2) oraz kolory tekstu wszystkich bloków i przycisku (R.4.4) są ustawione.

    styl.css CSS zmienione: 5 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    
    footer {
        background: linear-gradient(var(--green-light), var(--green-dark));
        color: white;
        padding: 12px;
    }
    
    footer p {
        margin: 0;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
    }
    
    video {
        width: 100%;
    }
    
    form {
        padding: 10px 50px;
    }
    
    button {
        background-color: var(--green-dark);
        color: white;
    }
    Podgląd strony po tym kroku (index.php)
  35. Krok 35: Przycisk po najechaniu kursorem i z klawiatury

    Kryteria:

    R.4.10 W momencie, gdy kursor myszy znajdzie się na przycisku jego kolory są zmienione na: biały kolor tła, kolor czcionki #97b498 (np. za pomocą pseudoklasy :hover)

    ok. 1 min
    Polecenie z arkusza

    W momencie, gdy kursor myszy znajdzie się na przycisku kolory tego przycisku są zmienione na: biały kolor tła, kolor czcionki #97b498

    :hover zamienia kolory pod kursorem (R.4.10). Ten sam wygląd dostaje :focus-visible - przycisk wybrany klawiszem Tab. Osoba, która nie używa myszy, też widzi, który element jest aktywny.

    styl.css CSS zmienione: 6 linii
    :root {
        --green-dark: #97b498;
        --green-light: #c8e6c9;
        --text-dark: #005005;
    }
    
    * {
        font-family: Garamond, serif;
    }
    
    header {
        background: linear-gradient(var(--green-dark), var(--green-light));
        color: white;
        text-align: center;
        padding: 5px;
    }
    
    #left {
        background-color: var(--green-light);
        color: var(--text-dark);
        height: 600px;
    }
    
    #right {
        color: var(--text-dark);
        height: 600px;
        overflow: scroll;
    }
    
    footer {
        background: linear-gradient(var(--green-light), var(--green-dark));
        color: white;
        padding: 12px;
    }
    
    footer p {
        margin: 0;
    }
    
    main {
        display: grid;
        grid-template-columns: 40% 60%;
    }
    
    video {
        width: 100%;
    }
    
    form {
        padding: 10px 50px;
    }
    
    button {
        background-color: var(--green-dark);
        color: white;
    }
    
    button:hover,
    button:focus-visible {
        background-color: white;
        color: var(--green-dark);
    }
    Podgląd strony po tym kroku (index.php)
  36. Etap 4: Skrypty PHP

    Kroki 36-43 · po tym etapie masz 42 z 42 kryteriów (pkt)

    Krok 36: Kod PHP na górze pliku i połączenie z bazą forumpsy

    Kryteria:

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

    ok. 4 min
    Polecenie z arkusza

    Skrypt łączy się z serwerem bazy danych na localhost, użytkownik root bez hasła, baza danych o nazwie forumpsy

    Trzy skrypty arkusza mają wspólną część - bazę. Cały kod PHP stoi więc na samej górze index.php, przed <!DOCTYPE html>: najpierw komunikacja z bazą, potem HTML, który tylko wypisuje przygotowane dane. W HTML-u przybyły trzy linie pod formularzem: jeśli jest komunikat, wypisz go w paragrafie.

    • e() - krótka funkcja z htmlspecialchars: każdy tekst wypisywany na stronie zamienia <, >, & i cudzysłowy na encje, więc odpowiedź ze znacznikami nie zamieni się w kod strony,
    • new PDO(...) - serwer localhost, baza forumpsy i kodowanie w jednym napisie, potem root i puste hasło (R.5.1). ERRMODE_EXCEPTION - każdy błąd bazy rzuca wyjątek, FETCH_ASSOC - wiersze przychodzą jako tablice z nazwami kolumn ($row['nick'] zamiast $row[2]),
    • try / catch - gdy baza nie działa, użytkownik dostaje zwykłe zdanie, a nie komunikat PHP z danymi serwera.

    PDO zamiast funkcji mysqli z tabeli 1 - klucz przyjmuje obie biblioteki (uwaga przy R.5). Od tego kroku podgląd to wynik prawdziwego uruchomienia strony.

    index.php PHP zmienione: 20 linii
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  37. Krok 37: Skrypt 1: zapytanie 3 i nagłówek z nickiem

    Kryteria:

    R.5.3 Skrypt 1 wysyła do bazy zapytanie 3 lub skrypt 3 wysyła do bazy zapytanie 4

    ok. 3 min
    Polecenie z arkusza

    Działanie skryptu 1: Wysyła do bazy zapytanie 3. Dane zwrócone zapytaniem wyświetlane są według wzoru, gdzie w nawiasach <> zapisano wartości pobrane z bazy: „Użytkownik: <nick>” w nagłówku czwartego stopnia

    • $pdo->query(...) - zapytanie 3 bez zmian (R.5.3); nie ma w nim danych od użytkownika, więc wystarczy zwykłe query,
    • ->fetch() - jeden wiersz, bo pytanie o id 1 jest jedno. Gdy pytania by nie było, fetch zwraca false - dlatego zmienna $question jest na początku ustawiona na false, a HTML wypisuje nagłówek w warunku if ($question),
    • w HTML-u <?= e($question['nick']) ?> - skrót <?= ?> to echo, a e() zabezpiecza nick.

    Podgląd: „Użytkownik: Jacek” pod avatarem.

    index.php PHP zmienione: 8 linii
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  38. Krok 38: Skrypt 1: liczba postów i treść pytania w paragrafach

    Kryteria: ,

    R.5.4 Skrypt 1 wyświetla dane wg wzoru: w h4: "Użytkownik: <nick>", w paragrafie: "<liczba postów> postów na forum”, w paragrafie <pytanie>, gdzie w nawiasach <> zapisano wartości pobrane z bazy

    R.2.6 W bloku banera zapisano nagłówek h1: "Forum miłośników psów", w bloku prawym h2: "Odpowiedzi na pytanie" oraz zapisano przynajmniej jeden paragraf (w skrypcie lub HTML)

    ok. 2 min
    Polecenie z arkusza

    „<liczba postów> postów na forum” w paragrafie. <pytanie> w paragrafie

    Dwa paragrafy w tym samym warunku (R.5.4, R.2.6). Liczba postów przechodzi przez (int) - liczba całkowita nie zawiera znaczników, więc nie potrzebuje e(); treść pytania - przez e().

    index.php PHP zmienione: 2 linie
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
                <p><?= (int) $question['postow'] ?> postów na forum</p>
                <p><?= e($question['pytanie']) ?></p>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  39. Krok 39: Skrypt 2: odpowiedź z formularza zapytaniem przygotowanym

    Kryteria: ,

    R.5.5 Skrypt 2 wysyła do bazy zmodyfikowane zapytanie 1 ten sposób, że w polu odpowiedz znajduje się wartość z pola tekstowego, należy sprawdzić w phpMyAdmin

    R.5.6 Jeżeli pole tekstowe jest puste, skrypt 2 nie wysyła danych do bazy (w skrypcie 2 znajduje się warunek sprawdzający czy pole jest puste)

    ok. 4 min
    Polecenie z arkusza

    Działanie skryptu 2: Jeżeli wpisano dane do pola wielowierszowego i dane nie są puste, skrypt wysyła do bazy zmodyfikowane zapytanie 1, w ten sposób, że w polu odpowiedz znajduje się wartość z pola wielowierszowego

    Skrypt 2 stoi przed zapytaniami 3 i 4 - nowa odpowiedź jest już w bazie, gdy skrypt 3 będzie czytał listę:

    • $_SERVER['REQUEST_METHOD'] === 'POST' - tylko po wysłaniu formularza,
    • trim($_POST['odpowiedz'] ?? '') - ?? daje pusty napis, gdy pola nie wysłano, a trim zdejmuje spacje i entery z brzegów: odpowiedź z samych spacji jest pusta,
    • === '' - pusta odpowiedź nie trafia do bazy (R.5.6), a pod formularzem pojawia się komunikat. empty() uznałby za puste także odpowiedź „0”,
    • mb_strlen(...) > 1000 - ten sam limit co maxlength w polu, sprawdzony na serwerze,
    • prepare z parametrem :odpowiedz i execute z treścią - zapytanie 1, w którym w miejscu treści stoi wartość z pola (R.5.5). Baza dostaje dane osobno od zapytania, więc apostrof w odpowiedzi niczego nie psuje.

    Sprawdzenie, jak każe klucz: po wysłaniu formularza w tabeli jest wiersz 9 z treścią z formularza. Podgląd: odpowiedź „Do sznaucera kup trymer…” wysłana - lista jeszcze pusta, wypełni ją skrypt 3.

    index.php PHP zmienione: 14 linii
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 2 - nowa odpowiedź z formularza, przed odczytem listy (od razu będzie na liście)
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $answer = trim($_POST['odpowiedz'] ?? '');
    
            if ($answer === '') {
                $message = 'Wpisz treść odpowiedzi - pusta odpowiedź nie została dodana.';
            } elseif (mb_strlen($answer) > 1000) {
                $message = 'Odpowiedź może mieć najwyżej 1000 znaków.';
            } else {
                $statement = $pdo->prepare('INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz) VALUES (1, 5, :odpowiedz);');
                $statement->execute(['odpowiedz' => $answer]);
            }
        }
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
                <p><?= (int) $question['postow'] ?> postów na forum</p>
                <p><?= e($question['pytanie']) ?></p>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>

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

  40. Krok 40: Skrypt 3: zapytanie 4 i elementy listy

    Kryteria: ,

    R.5.8 Skrypt 3 wyświetla dane w elementach listy

    R.2.7 W bloku prawym umieszczono listę numerowaną, zastosowano znaczniki <ol>, <li> (w skrypcie lub HTML)

    ok. 3 min
    Polecenie z arkusza

    Działanie skryptu 3: Wysyła do bazy zapytanie 4. Każdy wiersz zwrócony zapytaniem jest wyświetlany w elemencie listy

    • fetchAll() - od razu tablica wszystkich wierszy zapytania 4, bez pętli while przy bazie,
    • w HTML-u pętla foreach w składni alternatywnej (foreach (...): … endforeach;) - przy przeplataniu PHP z HTML-em łatwiej ją czytać niż klamry,
    • każdy wiersz w <li> z treścią odpowiedzi (R.5.8, R.2.7).

    Podgląd: cztery ponumerowane odpowiedzi, w tym ta z zapytania 1.

    index.php PHP zmienione: 8 linii
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    $answers = [];
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 2 - nowa odpowiedź z formularza, przed odczytem listy (od razu będzie na liście)
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $answer = trim($_POST['odpowiedz'] ?? '');
    
            if ($answer === '') {
                $message = 'Wpisz treść odpowiedzi - pusta odpowiedź nie została dodana.';
            } elseif (mb_strlen($answer) > 1000) {
                $message = 'Odpowiedź może mieć najwyżej 1000 znaków.';
            } else {
                $statement = $pdo->prepare('INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz) VALUES (1, 5, :odpowiedz);');
                $statement->execute(['odpowiedz' => $answer]);
            }
        }
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    
        // skrypt 3 - zapytanie 4: odpowiedzi na pytanie nr 1
        $answers = $pdo->query('SELECT odpowiedzi.id, odpowiedz, nick FROM odpowiedzi JOIN konta ON odpowiedzi.konta_id = konta.id WHERE odpowiedzi.Pytania_id = 1;')
            ->fetchAll();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
                <p><?= (int) $question['postow'] ?> postów na forum</p>
                <p><?= e($question['pytanie']) ?></p>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
    <?php foreach ($answers as $row): ?>
                    <li><?= e($row['odpowiedz']) ?></li>
    <?php endforeach; ?>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  41. Krok 41: Skrypt 3: nick w znaczniku em i linia pozioma

    Kryteria: ,

    R.5.7 Skrypt 3 wyświetla pola odpowiedz i nick każdego zwróconego wiersza

    R.3.7 W bloku prawym nicki osób publikujących odpowiedzi są wyróżnione znacznikiem <em> </em> (w skrypcie lub HTML)

    ok. 2 min
    Polecenie z arkusza

    Wyświetlane jest pole odpowiedz, pole nick oraz linia pozioma. Nick jest zapisany za pomocą znacznika semantycznego pochylającego tekst i oznaczającego tekst wyróżniony

    Nick w <em> (tekst wyróżniony, domyślnie kursywą - R.3.7), pod odpowiedzią <hr>. Odpowiedź i nick, oba przez e() (R.5.7).

    index.php PHP zmienione: 1 linia
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    $answers = [];
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 2 - nowa odpowiedź z formularza, przed odczytem listy (od razu będzie na liście)
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $answer = trim($_POST['odpowiedz'] ?? '');
    
            if ($answer === '') {
                $message = 'Wpisz treść odpowiedzi - pusta odpowiedź nie została dodana.';
            } elseif (mb_strlen($answer) > 1000) {
                $message = 'Odpowiedź może mieć najwyżej 1000 znaków.';
            } else {
                $statement = $pdo->prepare('INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz) VALUES (1, 5, :odpowiedz);');
                $statement->execute(['odpowiedz' => $answer]);
            }
        }
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    
        // skrypt 3 - zapytanie 4: odpowiedzi na pytanie nr 1
        $answers = $pdo->query('SELECT odpowiedzi.id, odpowiedz, nick FROM odpowiedzi JOIN konta ON odpowiedzi.konta_id = konta.id WHERE odpowiedzi.Pytania_id = 1;')
            ->fetchAll();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
                <p><?= (int) $question['postow'] ?> postów na forum</p>
                <p><?= e($question['pytanie']) ?></p>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
    <?php foreach ($answers as $row): ?>
                    <li><?= e($row['odpowiedz']) ?> <em><?= e($row['nick']) ?></em><hr></li>
    <?php endforeach; ?>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  42. Krok 42: Zamknięcie połączenia z bazą

    Kryteria:

    R.5.2 Ostatnią operacją na bazie jest jej zamknięcie (zgodne ze składnią)

    ok. 2 min
    Polecenie z arkusza

    Po wykonaniu operacji na bazie danych skrypt zamyka połączenie z serwerem.

    PDO nie ma funkcji close - połączenie zamyka się, gdy znika ostatnie odwołanie do obiektu: $statement = null i $pdo = null. Stoją w bloku finally, który wykona się zawsze po try - po udanym zapisie, po pustej odpowiedzi i po wyjątku (R.5.2).

    Dane pytania i lista odpowiedzi są już w zmiennych $question i $answers, więc HTML wypisuje je po zamknięciu połączenia. Podgląd: wysłana odpowiedź jest piąta na liście, podpisana nickiem Gosia (użytkownik nr 5).

    index.php PHP zmienione: 4 linie
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    $answers = [];
    $statement = null;
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 2 - nowa odpowiedź z formularza, przed odczytem listy (od razu będzie na liście)
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $answer = trim($_POST['odpowiedz'] ?? '');
    
            if ($answer === '') {
                $message = 'Wpisz treść odpowiedzi - pusta odpowiedź nie została dodana.';
            } elseif (mb_strlen($answer) > 1000) {
                $message = 'Odpowiedź może mieć najwyżej 1000 znaków.';
            } else {
                $statement = $pdo->prepare('INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz) VALUES (1, 5, :odpowiedz);');
                $statement->execute(['odpowiedz' => $answer]);
            }
        }
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    
        // skrypt 3 - zapytanie 4: odpowiedzi na pytanie nr 1
        $answers = $pdo->query('SELECT odpowiedzi.id, odpowiedz, nick FROM odpowiedzi JOIN konta ON odpowiedzi.konta_id = konta.id WHERE odpowiedzi.Pytania_id = 1;')
            ->fetchAll();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    } finally {
        $statement = null;
        $pdo = null;    // zamknięcie połączenia - ostatnia operacja na bazie
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
                <p><?= (int) $question['postow'] ?> postów na forum</p>
                <p><?= e($question['pytanie']) ?></p>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
    <?php foreach ($answers as $row): ?>
                    <li><?= e($row['odpowiedz']) ?> <em><?= e($row['nick']) ?></em><hr></li>
    <?php endforeach; ?>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (index.php)
  43. Krok 43: Odświeżenie po dodaniu odpowiedzi nie dodaje jej drugi raz

    ok. 3 min

    Dodatek spoza arkusza - klucz go nie ocenia. Strona po wysłaniu formularza jest wynikiem żądania POST, więc odświeżenie (F5) wysyła tę samą odpowiedź jeszcze raz - na liście pojawia się dubel. Wzorzec PRG (Post/Redirect/Get):

    • po udanym zapisie $answerAdded = true,
    • po bloku finally (połączenie jest już zamknięte) header('Location: index.php') każe przeglądarce wczytać stronę jeszcze raz zwykłym żądaniem GET, a exit kończy skrypt - header tylko ustawia nagłówek i nie zatrzymuje kodu,
    • przy pustej odpowiedzi przekierowania nie ma - strona zostaje z komunikatem.

    Nagłówek musi wyjść przed pierwszym znakiem HTML - dlatego kod PHP stoi na samej górze pliku. Sprawdzone na prawdziwym PHP: wysłanie formularza daje odpowiedź 302, potem lista z nową odpowiedzią, a odświeżenie nie dodaje kolejnego wiersza. Podgląd: stan po przekierowaniu.

    index.php PHP zmienione: 8 linii
    <?php
    function e(string $text): string
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
    
    $message = null;
    $question = false;
    $answers = [];
    $statement = null;
    $answerAdded = false;
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=forumpsy;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // skrypt 2 - nowa odpowiedź z formularza, przed odczytem listy (od razu będzie na liście)
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $answer = trim($_POST['odpowiedz'] ?? '');
    
            if ($answer === '') {
                $message = 'Wpisz treść odpowiedzi - pusta odpowiedź nie została dodana.';
            } elseif (mb_strlen($answer) > 1000) {
                $message = 'Odpowiedź może mieć najwyżej 1000 znaków.';
            } else {
                $statement = $pdo->prepare('INSERT INTO odpowiedzi (Pytania_id, konta_id, odpowiedz) VALUES (1, 5, :odpowiedz);');
                $statement->execute(['odpowiedz' => $answer]);
                $answerAdded = true;
            }
        }
    
        // skrypt 1 - zapytanie 3: autor pytania nr 1
        $question = $pdo->query('SELECT nick, postow, pytanie FROM konta JOIN pytania ON konta.id = pytania.konta_id WHERE pytania.id = 1;')
            ->fetch();
    
        // skrypt 3 - zapytanie 4: odpowiedzi na pytanie nr 1
        $answers = $pdo->query('SELECT odpowiedzi.id, odpowiedz, nick FROM odpowiedzi JOIN konta ON odpowiedzi.konta_id = konta.id WHERE odpowiedzi.Pytania_id = 1;')
            ->fetchAll();
    } catch (PDOException $exception) {
        $message = 'Nie udało się połączyć z bazą danych, spróbuj ponownie za chwilę.';
    } finally {
        $statement = null;
        $pdo = null;    // zamknięcie połączenia - ostatnia operacja na bazie
    }
    
    // po dodaniu odpowiedzi przekierowanie (PRG) - odświeżenie strony nie wyśle formularza ponownie
    if ($answerAdded) {
        header('Location: index.php');
        exit;
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Forum o psach</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <header>
            <h1>Forum miłośników psów</h1>
        </header>
        <main>
            <section id="left">
                <img src="Avatar.png" alt="Użytkownik forum" width="100" height="100">
    <?php if ($question): ?>
                <h4>Użytkownik: <?= e($question['nick']) ?></h4>
                <p><?= (int) $question['postow'] ?> postów na forum</p>
                <p><?= e($question['pytanie']) ?></p>
    <?php endif; ?>
                <video src="video.mp4" controls loop></video>
            </section>
            <section id="right">
                <form method="post">
                    <textarea name="odpowiedz" rows="4" cols="40" maxlength="1000" aria-label="Treść odpowiedzi"></textarea><br>
                    <button type="submit">Dodaj odpowiedź</button>
                </form>
    <?php if ($message !== null): ?>
                <p><?= e($message) ?></p>
    <?php endif; ?>
                <h2>Odpowiedzi na pytanie</h2>
                <ol>
    <?php foreach ($answers as $row): ?>
                    <li><?= e($row['odpowiedz']) ?> <em><?= e($row['nick']) ?></em><hr></li>
    <?php endforeach; ?>
                </ol>
            </section>
        </main>
        <footer>
            <p>
                Autor: Bartosz Bryniarski
                <a href="http://mojestrony.pl/" target="_blank" rel="noopener">Zobacz nasze realizacje</a>
            </p>
        </footer>
    </body>
    </html>

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

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

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

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

    ok. 3 min

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

    wersja podstawowawersja nowoczesna
    układfloat: left sekcji, clear stopki<main> z siatką
    szerokości 40% i 60%width na blokachgrid-template-columns
    kolorywpisane w każdej regulezmienne --green-dark, --green-light, --text-dark
    bibliotekamysqli (tabela 1 arkusza)PDO z wyjątkami
    gdzie jest kodtrzy bloki PHP w HTML-una górze pliku, w HTML-u tylko wypisanie
    odczyt wierszymysqli_fetch_row, $row[2]fetch, fetchAll, $row['nick']
    pusta odpowiedź!empty(), odpowiedź ze spacji przechodzitrim i === '', komunikat
    INSERTtreść wklejona w zapytanieparametr :odpowiedz
    wypisywanie danychecho wprosthtmlspecialchars przy każdej wartości
    zamknięcie bazymysqli_close po liście$pdo = null w finally
    po wysłaniuF5 dodaje odpowiedź drugi razprzekierowanie

    W prawdziwym forum odpowiedź byłaby zapisywana jako zalogowany użytkownik, a nie zawsze jako konto nr 5 - arkusz upraszcza ten fragment, więc obie wersje trzymają się zapytania 1.

    Na egzaminie wersja podstawowa trzyma się funkcji z tabeli 1 i łatwiej ją napisać bez dokumentacji. Nowoczesna nie wywraca się na apostrofie, nie wykonuje wpisanych skryptów i nie dubluje odpowiedzi po odświeżeniu - a klucz przyjmuje obie.

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

  45. Etap 6: Oddanie pracy

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

    Krok 45: Plik przeglądarka.txt i nagranie płyty

    ok. 5 min
    Polecenie z arkusza

    UWAGA: po zakończeniu pracy utwórz w folderze z numerem, którym został podpisany arkusz, plik tekstowy o nazwie przeglądarka.txt. Zapisz w nim nazwę przeglądarki internetowej, w której weryfikowałeś poprawność działania witryny. Nagraj płytę z rezultatami pracy.

    W przeglądarka.txt nazwa przeglądarki, w której sprawdzana była strona. W folderze z numerem zdającego: Avatar.png, import.jpg, index.php, kw1.png - kw4.png, kwerendy.txt, przeglądarka.txt, styl.css i video.mp4. Nagraj płytę, sprawdź jej odczyt i opisz ją numerem zdającego.

    przeglądarka.txt Tekst nowy plik
    Google Chrome

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

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.

Potwierdzenie

Informacja