Archiwum kategorii: PHP

Kurs programowania w języku PHP

PHP i bazy danych

Wprowadzenie

Bazy danych odgrywają kluczową rolę w tworzeniu dynamicznych aplikacji webowych. PHP, jako popularny język skryptowy, oferuje wbudowane rozszerzenia umożliwiające komunikację z bazami danych – w szczególności z systemem MySQL. W przeszłości używano starszego rozszerzenia MySQL (funkcje typu mysql_connect()), ale zostało ono uznane za przestarzałe już w 2012 roku​. Obecnie do obsługi MySQL w PHP wykorzystujemy MySQLi (ang. MySQL Improved) lub PDO (ang. PHP Data Objects). Niniejszy wykład przeznaczony jest dla początkujących i średniozaawansowanych programistów PHP i omawia szczegółowo, jak połączyć się z bazą danych oraz wykonywać na niej operacje, z naciskiem na dobre praktyki i bezpieczeństwo.

MySQLi vs PDO: Oba nowoczesne rozwiązania mają swoje zalety. MySQLi jest dedykowane tylko dla bazy MySQL (nie obsłuży innych silników baz danych), natomiast PDO stanowi uniwersalną warstwę abstrakcji pozwalającą na komunikację z wieloma różnymi bazami (obsługuje ponad 12 różnych systemów baz danych)​. Dzięki PDO potencjalna migracja aplikacji z MySQL na inny system (np. PostgreSQL) jest łatwiejsza – często wystarczy zmienić ciąg połączenia i ewentualnie kilka zapytań, zamiast przepisywać całość kodu​. MySQLi oferuje zarówno interfejs obiektowy, jak i proceduralny, podczas gdy PDO jest dostępne tylko w stylu obiektowym​. Co ważne, oba podejścia wspierają mechanizm tzw. prepared statements (zapytania z parametrami), które zabezpieczają przed atakami SQL injection – ten temat omówimy szerzej w dalszej części wykładu.

W kolejnych sekcjach pokażemy, jak nawiązać połączenie z bazą MySQL przy użyciu MySQLi i PDO, jak wykonywać podstawowe operacje CRUD (wynikające z ang. Create, Read, Update, Delete – czyli Insert, Select, Update, Delete), porównamy MySQLi z PDO, omówimy obsługę błędów, wykorzystanie zapytań z parametrami oraz najlepsze praktyki związane z bezpieczeństwem i organizacją kodu.

Tworzenie połączenia z bazą danych MySQL w PHP

Aby korzystać z bazy danych w PHP, najpierw musimy nawiązać połączenie z serwerem bazy danych i wybrać odpowiednią bazę. Zakładamy, że na serwerze MySQL została utworzona przykładowa baza danych (np. my_database) oraz że posiadamy dane dostępowe: host (np. localhost), nazwę użytkownika, hasło i nazwę bazy. Pokażemy dwa sposoby połączenia: za pomocą MySQLi oraz PDO.

Połączenie za pomocą MySQLi

W przypadku MySQLi można korzystać ze stylu obiektowego lub proceduralnego. Skupimy się na stylu obiektowym (klasa mysqli), ponieważ jest on zbliżony koncepcją do PDO i ułatwia to porównanie. Przykład poniżej pokazuje, jak utworzyć połączenie:

 <?php
 // Dane konfiguracyjne bazy danych
 $host     = "localhost";
 $user     = "uzytkownik";    // nazwa użytkownika bazy
 $password = "haslo";         // hasło użytkownika bazy
 $database = "my_database";   // nazwa bazy danych

 // Utworzenie połączenia (obiekt MySQLi)
 $conn = new mysqli($host, $user, $password, $database);

 // Ustawienie zestawu znaków na UTF-8 (zalecane dla polskich znaków i bezpieczeństwa)
 $conn->set_charset("utf8mb4");

 // Sprawdzenie, czy połączenie się powiodło
 if ($conn->connect_error) {
    die("Błąd połączenia: " . $conn->connect_error);
 }

 echo "Połączenie z bazą danych MySQL za pomocą MySQLi powiodło się!";
 ?>

W powyższym kodzie tworzymy nowy obiekt mysqli, przekazując do konstruktora kolejno host, nazwę użytkownika, hasło oraz nazwę bazy danych. Jeśli chcemy najpierw połączyć się z serwerem bez wybierania bazy, możemy pominąć nazwę bazy w konstruktorze – jednak zazwyczaj od razu podaje się bazę, z której będziemy korzystać. Zalecane jest również ustawienie odpowiedniego zestawu znaków (encoding) na połączeniu, np. UTF-8 (utf8mb4), aby zapewnić poprawne działanie polskich znaków i zabezpieczyć się przed potencjalnymi problemami z kodowaniem danych.

Po wykonaniu $conn = new mysqli(...) warto sprawdzić, czy połączenie doszło do skutku. Obiekt mysqli posiada właściwość $connect_error (oraz odpowiadającą jej metodę connect_error() w stylu proceduralnym), która zawiera komunikat błędu w przypadku nieudanej próby logowania. W powyższym kodzie, jeśli wystąpił błąd, przerywamy działanie skryptu poprzez die() i wyświetlamy komunikat. W środowisku produkcyjnym zamiast die() lepiej jest obsłużyć błąd w inny sposób (np. logując go), ale na etapie nauki lub w prostych skryptach takie rozwiązanie pozwala szybko wykryć problem.

Uwaga: MySQLi domyślnie nie rzuca wyjątków przy błędach, dlatego musimy sprawdzać błędy ręcznie (np. connect_error czy później $conn->error po wykonaniu zapytań). Istnieje możliwość włączenia stylu wyjątków w MySQLi (np. poprzez mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT) – o czym później), ale jest to opcjonalne.

Połączenie za pomocą PDO

PDO oferuje jednolity interfejs do wielu baz danych, w tym MySQL. Aby nawiązać połączenie, tworzymy instancję klasy PDO. Musimy podać tzw. DSN (Data Source Name), czyli specjalny ciąg zawierający informacje o rodzaju bazy, hoście, nazwie bazy itp., a także nazwę użytkownika i hasło. Przykład:

 <?php
 // Dane konfiguracyjne bazy (jak wcześniej)
 $host     = "localhost";
 $user     = "uzytkownik";
 $password = "haslo";
 $database = "my_database";

 try {
    // Utworzenie połączenia PDO (DSN zawiera nazwę bazy i zestaw znaków)
    $dsn = "mysql:host=$host;dbname=$database;charset=utf8mb4";
    $pdo = new PDO($dsn, $user, $password);
    
    // Ustawienie trybu raportowania błędów na wyjątki
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    echo "Połączenie z bazą danych MySQL za pomocą PDO powiodło się!";
 } catch (PDOException $e) {
    // Obsługa błędu połączenia
    echo "Błąd połączenia: " . $e->getMessage();
 }
 ?>

W powyższym kodzie ciąg DSN ma format mysql:host=<host>;dbname=<baza>;charset=<znaki>. Ustawienie charset=utf8mb4 jest ważne, tak samo jak w MySQLi, aby komunikacja z bazą odbywała się w UTF-8. Następnie przekazujemy DSN, nazwę użytkownika i hasło do konstruktora PDO. Ponieważ próba utworzenia new PDO(...) może zgłosić wyjątek w przypadku błędu (np. błędne dane dostępowe), otaczamy tę operację w blok try-catch.

Warto zaraz po połączeniu ustawić atrybut PDO::ATTR_ERRMODE na PDO::ERRMODE_EXCEPTION, co spowoduje, że wszelkie błędy bazy danych będą zgłaszane jako wyjątki (PDO domyślnie w trybie silent lub warning, w zależności od ustawień, więc explicite wymuszamy wyjątki). Dzięki temu kod w bloku catch wychwyci nie tylko ewentualne błędy samego połączenia, ale również późniejsze błędy zapytań (jeśli będziemy je wykonywać w tym samym try-catch lub kolejnych).

Należy zauważyć pewną różnicę: w PDO już w momencie nawiązywania połączenia musimy podać nazwę bazy (dbname). Jeśli tego nie zrobimy, PDO zgłosi błąd – wymagane jest istnienie wskazanej bazy danych podczas łączenia. W przypadku MySQLi możliwe jest połączenie bez wyboru bazy (co odpowiada wykonaniu osobno komendy SELECT DATABASE() później), lecz zwykle i tak zaraz po zalogowaniu wybiera się jedną bazę docelową.

Podsumowanie połączenia: Zarówno MySQLi, jak i PDO po poprawnym wykonaniu zwracają obiekt połączenia ($conn lub $pdo), którego będziemy używać do wykonywania dalszych operacji na bazie. Pamiętajmy o zabezpieczeniu danych dostępowych – w kodzie powyżej zostały one jawnie wpisane dla czytelności, ale w praktyce trzyma się je poza głównym kodem (np. w oddzielnym pliku konfiguracyjnym). W dalszych przykładach zakładamy, że połączenie zostało już utworzone i zachowane w zmiennej (odpowiednio $conn dla MySQLi lub $pdo dla PDO).

Wykonywanie podstawowych zapytań SQL (SELECT, INSERT, UPDATE, DELETE)

Po ustanowieniu połączenia możemy wykonywać zapytania SQL do bazy. Operacje te dzielą się na odczytywanie danych (SELECT) oraz modyfikowanie danych (INSERT – dodawanie nowych rekordów, UPDATE – aktualizacja istniejących, DELETE – usuwanie). Pokażemy podstawowe przykłady tych zapytań w kodzie PHP. Na początek użyjemy podejścia „bezpośredniego” (tzn. wstrzykujemy zapytanie SQL jako ciąg znaków). Później omówimy korzystniejsze podejście z użyciem przygotowanych zapytań (prepared statements), ale ważne jest zrozumieć najpierw podstawy.

Przykłady zapytań z użyciem MySQLi

Zakładamy, że posiadamy obiekt połączenia MySQLi w zmiennej $conn (utworzonej jak wyżej). Możemy korzystać z metody $conn->query(...) do wykonywania zapytań, która zwraca obiekt wynikowy dla zapytań typu SELECT lub wartość boolean (TRUE/FALSE) dla zapytań modyfikujących.

  • SELECT – Pobieranie danych z bazy:
 // Przykładowe zapytanie SELECT: pobranie wszystkich użytkowników  
 $sql = "SELECT * FROM users"; 
 $result = $conn->query($sql); 
 if ($result) { // Sprawdź ile wierszy zwrócono 
    if ($result->num_rows > 0) { // Iteracja po rezultatach 
    while ($row = $result->fetch_assoc()) { 
        echo "ID: " . $row['id'] . ", Nazwa: " . $row['name'] . "<br>"; } 
    } else { 
        echo "Brak wyników zapytania."; 
    } // Zwolnienie pamięci wyniku 
    $result->free_result(); 
 } else { 
     echo "Błąd w zapytaniu SELECT: " . $conn->error; 
 } 

W powyższym kodzie tworzymy zapytanie SQL wybierające wszystkie kolumny ze wszystkich rekordów tabeli users. Metoda $conn->query() zwraca obiekt klasy mysqli_result w przypadku powodzenia. Sprawdzamy najpierw, czy $result nie jest wartością fałszywą (FALSE) – jeśli byłoby FALSE, oznacza to błąd w zapytaniu i wtedy wypisujemy komunikat błędu, korzystając z $conn->error. Jeśli zapytanie się powiodło, możemy sprawdzić liczbę zwróconych wierszy przez $result->num_rows. Następnie odczytujemy kolejne wiersze wyniku za pomocą $result->fetch_assoc(), który zwraca tablicę skojarzoną reprezentującą kolejny wiersz (klucze tablicy odpowiadają nazwom kolumn). Iterujemy w pętli while dopóki są rekordy, wypisując np. identyfikator i nazwę użytkownika. Na koniec, gdy wynik nie jest już potrzebny, wywołujemy $result->free_result() w celu zwolnienia zasobów (nie jest to absolutnie wymagane, bo po zakończeniu skryptu PHP sam posprząta, ale jest to dobra praktyka przy dużych wynikach lub długotrwałych skryptach).

  • INSERT – Dodawanie nowego rekordu do bazy:
 // Przykładowe zapytanie INSERT: dodanie nowego użytkownika  $sql = "INSERT INTO users (name, email) VALUES ('Jan Kowalski', 'jan@example.com')"; 
 if ($conn->query($sql) === TRUE) {     echo "Dodano nowy rekord użytkownika!"; // Możemy uzyskać ID dodanego rekordu 
    $newId = $conn->insert_id; 
    echo " ID nowego użytkownika: $newId"; 
 } else { 
    echo "Błąd podczas dodawania rekordu: " . $conn->error; 
 }

W tym przykładzie konstruujemy zapytanie SQL dodające użytkownika z imieniem i emailem do tabeli users. Metoda $conn->query() zwraca TRUE gdy operacja się powiedzie (dla zapytań, które nie zwracają danych). Sprawdzamy więc wynik porównując z TRUE (operator === jest tu użyty do pewnego rozróżnienia typu). W przypadku sukcesu, możemy np. pobrać identyfikator dodanego rekordu przez właściwość $conn->insert_id. Gdy nastąpi błąd (np. naruszenie unikalności klucza), wchodzimy w blok else i wypisujemy komunikat błędu.

  • UPDATE – Modyfikacja istniejących danych:
 // Przykładowe zapytanie UPDATE: zmiana adresu email użytkownika o id=5   
 $sql = "UPDATE users SET email='jan.nowy@example.com' WHERE id=5";  
 if ($conn->query($sql) === TRUE) { 
     echo "Zaktualizowano dane użytkownika."; 
     echo " Liczba zmienionych wierszy: " . $conn->affected_rows; 
 } else { 
     echo "Błąd podczas aktualizacji: " . $conn->error; 
 }

Tutaj zapytanie zmienia adres email użytkownika o identyfikatorze 5. Po wykonaniu sprawdzamy, czy $conn->query() zwróciło TRUE. Jeżeli tak, można dodatkowo sprawdzić ile wierszy zostało zmodyfikowanych poprzez $conn->affected_rows. Jeśli zapytanie nie zmieniło żadnego wiersza (np. podane dane były identyczne z istniejącymi lub nie istniał użytkownik o id=5), affected_rows może zwrócić 0 mimo braku błędu – warto mieć to na uwadze. W razie błędu (FALSE), wypisujemy komunikat z error.

  • DELETE – Usuwanie rekordów:
 // Przykładowe zapytanie DELETE: usunięcie użytkownika o id=5  
 $sql = "DELETE FROM users WHERE id=5"; 
 if ($conn->query($sql) === TRUE) {  
     echo "Usunięto rekord użytkownika."; 
     echo " Liczba usuniętych wierszy: " . $conn->affected_rows; 
 } else {  
     echo "Błąd podczas usuwania: " . $conn->error; 
 }

Operacja usunięcia jest analogiczna do update – sprawdzamy powodzenie i ewentualnie ilość usuniętych rekordów. Użycie klauzuli WHERE jest bardzo istotne; bez niej polecenie DELETE usunęłoby wszystkie wiersze tabeli!

Powyższe przykłady pokazują podstawowy schemat pracy z bazą: tworzymy zapytanie SQL w formie stringa, wykonujemy je przez metodę biblioteczną, a następnie weryfikujemy wynik. Warto zawsze obsłużyć sytuację błędną (stąd konstrukcje if/else sprawdzające wynik i wypisujące ewentualne błędy). W prostych skryptach wyświetlanie $conn->error jest pomocne podczas debugowania, natomiast w aplikacjach produkcyjnych raczej logujemy te błędy po stronie serwera, zamiast prezentować je użytkownikowi (kwestie bezpieczeństwa – o czym dalej).

Przykłady zapytań z użyciem PDO

Posiadając obiekt PDO w zmiennej $pdo, możemy wykonywać zapytania na kilka sposobów. PDO udostępnia m.in. metodę $pdo->query() (podobnie jak MySQLi) oraz $pdo->exec(). Różnica jest taka, że $pdo->query() zwraca obiekt klasy PDOStatement dla zapytań SELECT (lub innych zwracających dane), natomiast $pdo->exec() zwraca jedynie liczbę zmienionych wierszy i służy głównie do zapytań typu INSERT/UPDATE/DELETE. Dodatkowo, ponieważ użyliśmy trybu wyjątków w PDO, w razie błędu zapytania zostanie zgłoszony wyjątek PDOException. Dla czytelności będziemy używać bloku try-catch w przykładach PDO, choć w prostych przypadkach można polegać na globalnym obsługiwaniu wyjątków.

  • SELECT – Pobieranie danych:
 try { 
     $sql = "SELECT * FROM users"; 
     $stmt = $pdo->query($sql); // Pętla po wynikach (korzystamy z fetch(PDO::FETCH_ASSOC)) 
     while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 
         echo "ID: {$row['id']}, Nazwa: {$row['name']}<br>"; 
     } // Alternatywnie można pobrać wszystkie wyniki naraz: // 
     $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); 
 } catch (PDOException $e) { 
     echo "Błąd zapytania SELECT: " . $e->getMessage(); 
 } 

Powyżej wykonujemy zapytanie SELECT analogiczne do wcześniejszego. Metoda $pdo->query() zwraca obiekt $stmt (statement). Następnie wywołujemy $stmt->fetch(PDO::FETCH_ASSOC) w pętli, aby uzyskać kolejne wiersze wyników jako tablice asocjacyjne (klasa PDO oferuje różne tryby pobierania danych, PDO::FETCH_ASSOC jest jednym z najczęściej używanych – zwraca wynik jak mysqli_fetch_assoc). W komentarzu pokazano też alternatywę: można skorzystać z $stmt->fetchAll(...), aby od razu pobrać całą tablicę wyników jedną komendą. Pamiętajmy, że po wykorzystaniu wyniku możemy (choć nie musimy) zamknąć kursor wyniku poprzez $stmt->closeCursor() lub po prostu przypisać $stmt = null aby upewnić się, że zasoby zostały zwolnione.

  • INSERT – Dodawanie danych:
 try { 
     $sql = "INSERT INTO users (name, email) VALUES ('Anna Nowak', 'anna@example.com')"; 
     $rowsAffected = $pdo->exec($sql); 
     echo "Dodano rekordów: $rowsAffected"; 
     $newId = $pdo->lastInsertId(); 
     echo " ID nowego użytkownika: $newId"; 
 } catch (PDOException $e) { 
     echo "Błąd INSERT: " . $e->getMessage(); 
 }

Tutaj używamy metody $pdo->exec() do wykonania zapytania dodającego rekord. Zmienna $rowsAffected powinna zawierać liczbę dodanych wierszy (w tym wypadku 1, o ile operacja się powiodła). Metoda lastInsertId() pozwala pobrać identyfikator wygenerowany przez ostatnie zapytanie INSERT (pod warunkiem, że tabela ma kolumnę typu AUTO_INCREMENT). Jeśli wystąpi błąd (np. złamanie ograniczeń bazy), zostanie zgłoszony wyjątek i przechwycony w catch.

  • UPDATE / DELETE – Modyfikacja i usuwanie danych:
 try { 
     $sql = "UPDATE users SET email='anna.nowak@example.com' WHERE name='Anna Nowak'";  
     $rowsAffected = $pdo->exec($sql); 
     echo "Zaktualizowano rekordów: $rowsAffected"; 
     $sql = "DELETE FROM users WHERE id=10"; 
     $rowsDeleted = $pdo->exec($sql); 
     echo "Usunięto rekordów: $rowsDeleted"; 
 } catch (PDOException $e) { 
     echo "Błąd modyfikacji danych: " . $e->getMessage(); 
 }

W powyższym fragmencie w jednym bloku try wykonujemy dwa zapytania: najpierw aktualizacja (zmiana adresu email dla użytkownika o imieniu „Anna Nowak”), potem usunięcie użytkownika o id=10. pdo->exec() zwraca odpowiednio liczbę zmienionych lub usuniętych wierszy, co wykorzystujemy do poinformowania o wyniku. W razie jakiegokolwiek błędu w którymkolwiek zapytaniu, kontrola przejdzie do sekcji catch. Jeśli chcielibyśmy obsłużyć te operacje osobno, można oczywiście użyć dwóch odrębnych bloków try-catch.

Jak widać, składnia zapytań SQL jest identyczna niezależnie od tego, czy używamy MySQLi czy PDO – różnią się jedynie sposoby wywołania tych zapytań i obsługi wyników. MySQLi wymaga użycia różnych metod (np. query(), fetch_assoc(), właściwości jak affected_rows), podczas gdy PDO używa obiektu PDOStatement oraz metod takich jak fetch() czy rowCount(). Warto też zauważyć, że w powyższych przykładach PDO nie wymagało jawnego zamykania połączenia czy wyniku – po zakończeniu skryptu wszystko jest automatycznie zwalniane, a w przypadku braku dodatkowych odniesień do $stmt lub $pdo, PHP sam zwolni zasoby (choć o zamykaniu połączeń będzie więcej w sekcji o dobrych praktykach).

Uwaga: Przedstawione powyżej wykonywanie zapytań poprzez bezpośrednie wstawianie wartości do stringa SQL działa, ale nie jest bezpieczne, jeśli te wartości pochodzą od użytkownika. Na przykład, w zapytaniu INSERT wpisaliśmy na sztywno wartości 'Anna Nowak' i 'anna@example.com'. Gdyby jednak te dane pochodziły z formularza (np. $_POST), należałoby je odpowiednio zabezpieczyć. Najlepszym sposobem jest użycie przygotowanych zapytań (prepared statements), które omówimy za chwilę. Jeśli z jakiegoś powodu nie używamy prepared statements, absolutnym minimum jest filtrowanie lub eskejpownie danych wejściowych (np. mysqli_real_escape_string() dla każdej zmiennej wstawianej do SQL) – inaczej aplikacja będzie narażona na ataki SQL injection.

Różnice między MySQLi a PDO – wady i zalety

Wybór między MySQLi a PDO zależy od potrzeb projektu oraz preferencji programisty. Poniżej zestawiamy główne różnice, wraz z ich konsekwencjami:

  • Obsługiwane bazy danych: MySQLi działa tylko z bazą MySQL/MariaDB, natomiast PDO jest interfejsem uniwersalnym, obsługującym wiele silników baz danych (m.in. MySQL, PostgreSQL, SQLite, Microsoft SQL Server i inne). Jeśli istnieje potrzeba potencjalnego przeniesienia aplikacji na inną bazę lub jednoczesnej obsługi różnych baz, PDO daje taką elastyczność. W MySQLi zmiana bazy z MySQL na inną wymagałaby napisania dużej części kodu od nowa (w tym wszystkich zapytań)​.
  • Interfejs proceduralny vs obiektowy: MySQLi udostępnia dwie formy API – obiektową (przez klasę mysqli oraz obiekty wyników) i proceduralną (poprzez funkcje mysqli_*). Oba sposoby robią to samo, różnią się stylem kodowania. PDO natomiast jest wyłącznie obiektowe. Dla osób początkujących podejście proceduralne MySQLi bywa czasem łatwiejsze do zrozumienia (przypomina stary sposób użycia mysql_*), jednak podejście obiektowe jest bardziej nowoczesne i skaluje się lepiej w większych aplikacjach. Warto dodać, że obie biblioteki są obiektowe pod spodem – nawet używając MySQLi proceduralnie, PHP wewnętrznie i tak korzysta z obiektów. To tylko kwestia składni i organizacji kodu.
  • Składnia zapytań z parametrami: Obydwie biblioteki wspierają prepared statements (zapytania z parametrami), jednak różnią się nieco ich składnią. MySQLi w zapytaniach przygotowanych używa znaków zapytania ? jako tzw. anonimowych (pozycyjnych) miejsc na parametry. PDO domyślnie pozwala na nazwane parametry (np. :name, :email w treści SQL) lub również znaki ?. Nazwane parametry mogą poprawić czytelność zapytania (wiadomo, która zmienna jest w którym miejscu), lecz w PDO mają ograniczenie – nie można użyć dwa razy tej samej nazwy parametru w jednym zapytaniu, chyba że użyjemy emulacji (to detal, na który rzadko natknie się początkujący). Z kolei w MySQLi nie ma nazwanych parametrów – parametry są łączone z miejscami ? w kolejności.
  • Obsługa błędów: MySQLi domyślnie nie wyrzuca wyjątków, a jedynie sygnalizuje błędy poprzez kod i komunikat błędu. Trzeba pisać dodatkowy kod sprawdzający $mysqli->error lub wartości zwracane. Istnieje możliwość włączenia trybu wyjątków (poprzez mysqli_report() lub ustawienia konfiguracji), ale nie jest to standard zachowania. PDO natomiast oferuje łatwe przełączanie trybu błędów – możemy odbierać błędy jako ciche (ERRMODE_SILENT), jako ostrzeżenia (ERRMODE_WARNING), lub jako wyjątki (ERRMODE_EXCEPTION). Ustawienie na wyjątki (co zrobiliśmy wcześniej) jest bardzo wygodne, bo pozwala użyć mechanizmu try/catch, co upraszcza kontrolę przepływu w przypadku błędów.
  • Funkcjonalności dodatkowe: MySQLi posiada pewne funkcje i cechy specyficzne dla MySQL, których PDO może nie wspierać bezpośrednio. Przykłady to:
    • Asynchroniczne zapytania – MySQLi oferuje mechanizm wykonywania zapytań asynchronicznie (funkcje mysqli_poll, flagi MYSQLI_ASYNC itp.), co bywa przydatne w zaawansowanych przypadkach, gdy chcemy równolegle wykonać kilka zapytań. PDO takiego mechanizmu nie udostępnia.
    • Multi-query – MySQLi pozwala wykonać wiele zapytań naraz w jednym stringu SQL ($conn->multi_query("STATEMENT1; STATEMENT2;")). W PDO standardowo pojedyncze wywołanie dotyczy jednego zapytania; aby wykonać wiele na raz, musielibyśmy włączyć tryb emulacji i przesłać kilka poleceń po średniku, co nie zawsze jest zalecane. W praktyce jednak wielokrotne zapytania można zwykle wykonać kolejno, a multi-query bywa rzadko używane.
    • Pobieranie dodatkowych informacji o zapytaniu – MySQLi udostępnia np. metodę $conn->info czy funkcje do uzyskania liczby ostrzeżeń na serwerze, itp. PDO skupia się raczej na przenośnych aspektach i czasem dostęp do specyficznych informacji MySQL może wymagać wywołania zapytania SQL (np. SHOW WARNINGS).
    • Zamykanie połączenia – MySQLi oferuje metodę $conn->close(), podczas gdy PDO nie ma dedykowanej metody close(). Jednak w PDO zamknięcie połączenia następuje automatycznie przy usunięciu obiektu (wystarczy $pdo = null). Nie jest to więc brak funkcjonalności, a jedynie inny sposób zarządzania zasobami.
  • Wydajność: Czysto wydajnościowo MySQLi i PDO są do siebie zbliżone. Różnice w szybkości wykonania zapytań są zwykle pomijalne dla typowych zastosowań. Czasem pojawiają się dyskusje, czy jedna biblioteka jest szybsza od drugiej – testy wydajności pokazują minimalne różnice, które mogą się zmieniać w zależności od wersji PHP, sterowników czy sposobu użycia. Decyzji nie powinno się raczej opierać na wydajności, a na powyższych cechach (np. przenośność vs specyficzne funkcje).

Podsumowując, PDO jest często polecane jako domyślny wybór, zwłaszcza dla nowych projektów, ze względu na wszechstronność i wygodną obsługę błędów. MySQLi jednak również jest w pełni poprawnym wyborem, szczególnie jeśli wiemy, że nasza aplikacja zawsze będzie używać MySQL i chcemy skorzystać z jego najnowszych możliwości​. W wielu prostych zastosowaniach obie biblioteki sprawdzą się równie dobrze – ich składnia jest bardzo podobna (co widać było w przykładach), więc przesiadka z jednej na drugą nie jest trudna. Należy unikać jedynie mieszania obu na raz – wybierzmy jedną i konsekwentnie się jej trzymajmy w projekcie.

Obsługa błędów i wyjątki

Błędy w pracy z bazą danych są nieuniknione – mogą wynikać z literówek w SQL, naruszenia ograniczeń (np. próba dodania rekordu z kluczem, który już istnieje), utraty połączenia z serwerem i wielu innych przyczyn. Dlatego ważne jest, aby kod potrafił te błędy wykryć i odpowiednio obsłużyć, zamiast po prostu przerywać działanie lub, co gorsza, kontynuować z błędnymi założeniami.

MySQLi – sprawdzanie błędów: Jak wcześniej wspomniano, MySQLi nie rzuca wyjątków domyślnie. Sposób obsługi błędów polega na sprawdzaniu wartości zwracanej przez funkcje/metody oraz właściwości błędu. Kilka wskazówek:

  • Po wykonaniu $conn->query($sql) warto sprawdzić, czy wynik nie jest FALSE. Jeżeli jest, oznacza to błąd wykonania zapytania. Informacja o błędzie jest dostępna pod $conn->error (wiadomość tekstowa) oraz $conn->errno (kod błędu numeryczny). Przykład:
 $result = $conn->query($sql);  if ($result === FALSE) { 
      echo "Błąd zapytania: " . $conn->error . " (kod " . $conn->errno . ")"; 
 }

Przy próbie połączenia, jak widzieliśmy, można sprawdzać $conn->connect_error lub użyć funkcji proceduralnej mysqli_connect_error(). Jeśli połączenie się nie uda, nie ma sensu wykonywać dalszych zapytań – należy np. przerwać skrypt lub podjąć próbę ponownego połączenia po pewnym czasie (w aplikacjach produkcyjnych można zaimplementować np. kilkukrotne ponawianie połączenia zanim zgłosi się krytyczny błąd).

  • Wyjątki w MySQLi: Można włączyć tryb zgłaszania wyjątków przez MySQLi wywołując funkcję mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); przed utworzeniem połączenia. Wówczas błędy będą rzucane jako wyjątki typu mysqli_sql_exception. Przykład:
 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); 
 try { 
     $conn = new mysqli($host, $user, $password, $database); 
     $conn->set_charset("utf8mb4"); // ... wykonanie zapytań ... 
 } catch (mysqli_sql_exception $e) { 
     echo "Wystąpił błąd bazy danych: " . $e->getMessage(); 
 }

Takie podejście z try/catch w MySQLi upodabnia się do stylu PDO. Wybór trybu zależy od preferencji – wielu programistów trzyma się domyślnego sposobu (sprawdzanie błędów ręcznie), ale w większych projektach tryb wyjątków bywa wygodny.

PDO – obsługa wyjątków: Jeśli ustawimy PDO::ATTR_ERRMODE na PDO::ERRMODE_EXCEPTION (co zrobiliśmy przy połączeniu), to w zasadzie mamy prostą sprawę – każda nieudana operacja na bazie danych spowoduje rzucenie wyjątku PDOException. Możemy zatem opakować krytyczne operacje w blok try-catch:

 try {
     $pdo->query("BAD SQL SYNTAX"); // to zapytanie zawiera błąd składniowy
 } catch (PDOException $e) {
     echo "SQL Error: " . $e->getMessage();
 } 

Jeśli nie przechwycimy takiego wyjątku, a błąd wystąpi, skrypt zakończy się błędem (fatal error). W trakcie developmentu może to być zauważalne od razu, ale na produkcji nie chcemy takich „wyjątków nieobsłużonych”. Dlatego można zastosować globalny mechanizm przechwytywania wyjątków za pomocą set_exception_handler(), który przekieruje nieobsłużone wyjątki do specjalnej funkcji (np. logującej je i wyświetlającej przyjazny komunikat). PDOException, jak każdy wyjątek, może być też obsłużony w miejscu wystąpienia jak pokazano wyżej.

Nie ujawniaj szczegółów błędów użytkownikom: Bardzo ważna zasada – komunikaty błędów z bazy danych często zdradzają wewnętrzne szczegóły aplikacji lub zapytań SQL. W środowisku produkcyjnym nie należy ich wyświetlać bezpośrednio użytkownikom. Zamiast tego:

  • Można wyświetlić ogólny komunikat typu „Wystąpił błąd podczas operacji. Spróbuj ponownie później.” bez szczegółów.
  • Szczegóły techniczne błędu (np. pełny tekst zapytania, kod błędu, stack trace) powinny trafić do logów serwera, do których ma dostęp tylko deweloper/administrator.
  • Upewnij się, że w konfiguracji PHP opcja display_errors jest wyłączona (0) na serwerze produkcyjnym​, aby ewentualne niezłapane wyjątki nie wypisały się użytkownikowi wraz ze ścieżkami plików czy danymi połączenia. Zamiast tego stosuj log_errors włączone i zapis błędów do pliku dziennika.

Transakcje a błędy: Jeśli Twoja aplikacja używa transakcji (np. poprzez $conn->begin_transaction() w MySQLi lub $pdo->beginTransaction() w PDO), obsługa błędów powinna uwzględniać wycofanie transakcji w razie problemów. Przykładowo:

 $pdo->beginTransaction();
 try {
     // kilka zapytań, np. przeniesienie środków między kontami
     $pdo->exec($sql1);
     $pdo->exec($sql2);
     $pdo->commit();
 } catch (PDOException $e) {
     $pdo->rollBack();
     echo "Transakcja przerwana, dokonano rollback. Błąd: " . $e->getMessage();
 } 

Podobnie w MySQLi, po $conn->begin_transaction() w razie błędu należy wywołać $conn->rollback(). W ten sposób dane w bazie pozostaną spójne nawet, gdy część operacji się nie powiedzie.

Reasumując: zawsze sprawdzaj rezultaty operacji na bazie, używaj wyjątków lub instrukcji warunkowych do wychwycenia błędów i nigdy nie zakładaj, że dane wejściowe lub zapytania zawsze będą poprawne. Dobrze zaprojektowany kod potrafi obsłużyć błędne sytuacje w kontrolowany sposób.

Prepared statements (instrukcje przygotowane) – składnia, zastosowanie, korzyści

Prepared statements (zapytania przygotowane, czasem tłumaczone jako instrukcje parametryzowane) to mechanizm pozwalający oddzielić definicję zapytania SQL od danych, które do niego wstawiamy. Umożliwia to wielokrotne wykonanie tego samego zapytania z różnymi danymi w sposób bezpieczny i efektywny. W praktyce przygotowane zapytania rozwiązują problem SQL injection oraz mogą poprawić wydajność, gdy to samo zapytanie wykonujemy wielokrotnie.

Jak to działa? W pierwszym etapie wysyłamy do serwera bazodanowego szablon zapytania z pustymi miejscami na dane (tzw. placeholdery). Serwer analizuje i kompiluje zapytanie (ale go nie wykonuje), przygotowując miejsce na późniejsze podstawienie zmiennych. W drugim etapie przesyłamy konkretne wartości zmiennych i wydajemy polecenie wykonania wcześniej przygotowanego zapytania​php.net. Dzięki temu serwer wie, że przekazane wartości są danymi, a nie częścią kodu SQL, co chroni przed wstrzyknięciem złośliwego kodu. Ponadto, jeśli to zapytanie wykonujemy np. w pętli dla wielu różnych zestawów danych, baza nie musi za każdym razem kompilować SQL – może wykorzystać raz przygotowany plan wykonania, co oszczędza czas.

Zarówno MySQLi, jak i PDO obsługują ten tryb pracy. Spójrzmy na przykłady.

  • Prepared statements w MySQLi (obiektowo):
 // Przygotowanie zapytania z dwoma parametrami (imię i email) 
 $stmt = $conn->prepare("INSERT INTO users (name, email) VALUES (?, ?)"); 
 if (!$stmt) { 
     die("Błąd przygotowania zapytania: " . $conn->error); 
 } 
 // Powiązanie zmiennych z parametrami zapytania 
 $stmt->bind_param("ss", $name, $email); // "ss" oznacza dwa parametry typu string (string, string) 
 // Przypisanie wartości do zmiennych i wykonanie 
 $name = "Janek"; 
 $email = "janek@example.com"; 
 $stmt->execute(); // Ponowne wykorzystanie tego samego przygotowanego zapytania z innymi danymi: 
 $name = "Kasia"; 
 $email = "kasia@example.com"; 
 $stmt->execute(); 
 $stmt->close(); 

W powyższym kodzie najpierw wywołujemy $conn->prepare() z podanym szablonem zapytania SQL. W miejscu, gdzie normalnie pojawiłyby się wartości, wstawiliśmy znak zapytania ? – to właśnie placeholder. Funkcja prepare() zwraca obiekt mysqli_stmt (przygotowane zapytanie) lub FALSE, jeśli nastąpił błąd podczas przygotowania (np. złe SQL). Gdy mamy $stmt, wywołujemy $stmt->bind_param(), przekazując jako pierwszy argument łańcuch określający typy danych kolejnych parametrów. W naszym przypadku "ss" oznacza, że pierwszym parametrem będzie string (s), i drugim również string (s). Gdybyśmy przekazywali np. liczbę całkowitą i string, użylibyśmy "is" (i – integer, s – string). Dostępne oznaczenia to: i (int), d (double – liczba zmiennoprzecinkowa), s (string), b (blob, dane binarne). Następne argumenty bind_param to zmienne PHP, które mają zostać powiązane z parametrami. Ważne: wiążemy zmienne, nie konkretną wartość. Oznacza to, że jeśli później zmienimy wartość tych zmiennych i ponownie wykonamy zapytanie, to zostaną użyte nowe wartości. Powyżej widać to w akcji: najpierw przypisujemy "Janek", "janek@example.com" i wywołujemy $stmt->execute(), co spowoduje wstawienie tych danych do zapytania i dodanie rekordu do bazy. Następnie ustawiamy zmienne $name i $email na inne wartości i znów wywołujemy $stmt->execute(). To doda kolejny rekord, tym razem z imieniem Kasia. Nie trzeba ponownie wywoływać prepare(), a nawet bind_param() (chyba że chcemy powiązać inne zmienne lub zmienić typy) – możemy wielokrotnie wykonywać execute() z różnymi wartościami wcześniej powiązanych zmiennych​. Po zakończeniu pracy z przygotowanym zapytaniem, wywołujemy $stmt->close() aby zwolnić zasoby związane z tym zapytaniem. Oczywiście prepared statements można użyć także do zapytań SELECT. Wtedy po execute() należałoby pobrać wyniki. Można to zrobić na dwa sposoby: 1. Z użyciem metody $stmt->get_result(), która zwróci obiekt wynikowy podobny do tego z $conn->query(). Następnie używamy fetch_assoc() na tym wyniku. Uwaga: get_result() wymaga, aby PHP był skompilowany z natywnym sterownikiem MySQLND (większość współczesnych instalacji PHP go ma). 2. Z użyciem bind_result() i fetch() – wiązanie zmiennych do kolumn wyniku. To bardziej niskopoziomowe podejście: trzeba znać liczbę i typy kolumn, wywołać $stmt->bind_result($col1, $col2, ...), a następnie $stmt->fetch(), który ustawi te zmienne. W prostych zastosowaniach łatwiej jest skorzystać z get_result().

  • Prepared statements w PDO:
 // Przygotowanie zapytania SELECT z nazwanym parametrem :email  
 $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email"); 
 // Powiązanie wartości parametru i wykonanie 
 $emailParam = "janek@example.com"; 
 $stmt->execute(['email' => $emailParam]); 
 // Pobranie wyniku 
 $user = $stmt->fetch(PDO::FETCH_ASSOC); 
 echo "Znaleziono użytkownika: " . $user['name']; 
 $stmt->closeCursor();

Tutaj widzimy przygotowanie zapytania z użyciem nazwanego parametru :email. Metoda $pdo->prepare() zwraca obiekt PDOStatement lub rzuca wyjątek w razie poważnego błędu (np. składni SQL). Zakładamy, że się powiodło. Aby przekazać wartość do parametru, używamy metody $stmt->execute(). Można to zrobić na dwa sposoby w PDO: – Przekazując tablicę wartości – w formie indeksowanej (jeśli używaliśmy ?) lub asocjacyjnej (gdy używamy nazwanych parametrów). W powyższym przykładzie przekazujemy tablicę ['email' => $emailParam]. Klucz 'email' odpowiada nazwie parametru bez dwukropka (PDO automatycznie dopasuje to do :email w zapytaniu). To podejście jest szybkie i wygodne, bo łączy w sobie wiązanie i wykonanie w jednym kroku. – Alternatywnie można najpierw wywołać $stmt->bindParam(':email', $emailParam) osobno, a potem samo $stmt->execute() bez argumentów. bindParam w PDO pozwala też opcjonalnie określić typ danych (trzeci argument, np. PDO::PARAM_STR dla string, PDO::PARAM_INT dla integer itd.), chociaż PDO zazwyczaj sam dobrze rozpoznaje typ. Po wykonaniu zapytania SELECT, wynik jest gotowy do pobrania z $stmt. Możemy użyć $stmt->fetch() aby pobrać pojedynczy wiersz (jak zrobiliśmy powyżej, zakładając że email jest unikalny i dostaniemy co najwyżej jednego użytkownika), albo użyć pętli tak jak w poprzednich przykładach. Wykorzystujemy tryb FETCH_ASSOC by otrzymać tablicę asocjacyjną. Jeśli planujemy ponownie użyć tego PDOStatement do innego zapytania lub chcemy eksplicytnie zwolnić połączenie do serwera (w przypadku dużych wyników), można wywołać $stmt->closeCursor(), co zwolni serwer do obsługi innych zapytań na danym połączeniu. Korzyści i uwagi: Widzimy, że prepared statements w PDO są nieco prostsze w użyciu – nie trzeba podawać typów (jak "ss" w MySQLi), zazwyczaj nie trzeba wcześniej wiązać (można od razu przekazać tablicę do execute). Dodatkowo możliwość użycia nazwanych parametrów może poprawić czytelność zapytań, zwłaszcza jeśli jest ich wiele. Najważniejszą zaletą jest jednak bezpieczeństwo: niezależnie od tego, co znajduje się w zmiennej $emailParam, zostanie to przekazane do bazy w kontrolowany sposób jako wartość parametru, a nie część kodu. Oznacza to, że nawet gdyby w adresie e-mail ktoś próbował przemycić np. "' OR 1=1--" (klasyczny trik SQL injection), to serwer potraktuje to jako zwykły ciąg znaków (wartość szukanego email), a nie fragment zapytania. Innymi słowy, stosując prepared statements, zabezpieczamy się przed atakami SQL injection​. Warto wspomnieć, że prepared statements mogą też minimalnie poprawić wydajność przy wielokrotnym wykonywaniu tego samego zapytania. Jak pokazano w przykładzie MySQLi – przygotowaliśmy raz zapytanie INSERT i wykonaliśmy je dwa razy z różnymi danymi. Przy pierwszym wykonaniu baza danych skompilowała zapytanie, przy drugim mogła już skorzystać z wcześniejszego przygotowania, stosując tylko nowe wartości. Gdybyśmy to samo zrobili tradycyjnie, dwa razy musiałaby przetworzyć całe zapytanie od zera. Różnica przy dwóch wykonaniach jest pomijalna, ale przy setkach czy tysiącach powtórzeń może być odczuwalna (np. wstawianie wielu rekordów w pętli). Należy również wiedzieć, że PDO posiada mechanizm emulacji przygotowanych zapytań. Oznacza to, że w pewnych sytuacjach zapytanie nie jest przygotowywane po stronie serwera, tylko PDO sam podstawia wartości lokalnie i wykonuje zwykłe zapytanie. Domyślnie emulacja jest włączona dla niektórych sterowników, ale dla MySQL od lat domyślnie PDO używa prawdziwych prepared statements (można to zmienić atrybutem PDO::ATTR_EMULATE_PREPARES). W kontekście bezpieczeństwa emulowane i natywne prepared statements dają ten sam poziom ochrony przed SQL injection, jednak w szczególnych przypadkach emulacja może mieć wpływ na zachowanie (np. obsługa bardzo dużych danych typu BLOB). Dla typowych zastosowań nie musimy się tym przejmować.

Podsumowanie: Korzystanie z prepared statements jest zdecydowanie zalecane przy pracy z bazą danych. Jak stwierdza oficjalna dokumentacja PHP: „A prepared statement … is used to execute the same statement repeatedly with high efficiency and protect against SQL injections.”. Stosując ten mechanizm zyskujemy zarówno na bezpieczeństwie, jak i na czystości kodu (oddzielenie logiki SQL od danych). W kolejnej sekcji przyjrzymy się bliżej kwestii bezpieczeństwa i innym środkom ochrony.

Bezpieczeństwo – ochrona przed SQL injection i filtrowanie danych wejściowych

SQL injection (wstrzyknięcie SQL) to jedno z najpoważniejszych zagrożeń czyhających na aplikacje bazodanowe. Polega ono na takim manipulowaniu danymi wejściowymi aplikacji, by zmusić ją do wykonania niezamierzonego przez programistę zapytania SQL. Atakujący wstrzykuje do przesyłanych danych fragmenty kodu SQL. Jeśli aplikacja nie jest zabezpieczona i po prostu dokleja te dane do zapytań, baza danych może wykonać np. polecenie usunięcia tabeli lub ujawnienia danych, do których nie powinno być dostępu. W skrajnych przypadkach „SQL injection może zniszczyć bazę danych”​. Jest to jedna z najczęstszych technik ataków na aplikacje webowe​.

Przykład ataku: Wyobraźmy sobie formularz logowania, gdzie użytkownik podaje login i hasło. Na serwerze (błędnie) budowane jest zapytanie w stylu:

 $username = $_POST['user'];
 $password = $_POST['pass'];
 $sql = "SELECT * FROM users WHERE username='$username' AND password='$password'";

Jeśli intruz w polu użytkownika wpisze: admin' -- a hasło zostawi puste, to zapytanie stanie się:

 SELECT * FROM users WHERE username='admin' --' AND password=''

Znak -- w SQL oznacza początek komentarza, więc faktyczne zapytanie, jakie wykona baza, to:

SELECT * FROM users WHERE username='admin'

Hasło zostało skomentowane, czyli warunek hasła został pominięty! W efekcie atakujący zaloguje się jako „admin” nie znając hasła. To prosty przykład, ale pokazuje skalę problemu. W innym scenariuszu ktoś mógłby wstrzyknąć '; DROP TABLE users; -- co spowodowałoby usunięcie tabeli użytkowników z bazy.

Obrona przed SQL injection: Najskuteczniejszą obroną jest używanie przygotowanych zapytań (omówionych wyżej). Kiedy stosujemy parametryzowane zapytania, dane użytkownika nie są łączone z kodem SQL bezpośrednio, tylko przekazywane oddzielnie, co uniemożliwia interpretację tych danych jako polecenia​. Nawet jeśli w nazwie użytkownika będzie admin' --, to serwer potraktuje to jako zwykły ciąg znaków do porównania, a nie jako część składni.

Inne dobre praktyki zabezpieczające przed SQL injection i innymi zagrożeniami związanymi z danymi wejściowymi:

  • Walidacja i filtrowanie danych wejściowych: Zawsze sprawdzajmy, czy dane od użytkownika mają spodziewany format i zakres. Np. jeśli oczekujemy liczby (ID, wiek itp.), upewnijmy się że to faktycznie liczba (np. używając ctype_digit, rzutowania (int) lub filtra FILTER_VALIDATE_INT). Jeżeli oczekujemy adresu email – możemy skorzystać z filter_var($email, FILTER_VALIDATE_EMAIL) by sprawdzić poprawność. Filtrowanie może też polegać na usuwaniu niedozwolonych znaków (np. jeśli spodziewamy się tylko liter, odrzucamy wszystko inne).
  • Escapowanie znaków specjalnych: Jeżeli z jakiegoś powodu musimy jednak wbudować zmienną w zapytanie SQL (co powinno być ostatecznością), należy koniecznie użyć funkcji ucieczki znaków. Dla MySQLi będzie to mysqli_real_escape_string($conn, $zmienna), a dla PDO można użyć $pdo->quote($zmienna) lub funkcji PDO::quote (choć lepiej jednak przejść na prepared statements). Funkcje te poprzedzą specjalne znaki (jak cudzysłów, apostrof, backslash) odpowiednimi znakami ucieczki, co zapobiega złamaniu składni SQL. Samo escapowanie jednak bywa zawodne, jeśli zapomnimy go użyć w jednym miejscu – dlatego parametryzacja jest bezpieczniejsza, bo wymusza rozdział danych.
  • Ograniczanie uprawnień w bazie: To nie dotyczy bezpośrednio kodowania w PHP, ale jest ważną częścią bezpieczeństwa. Konto bazy danych, którego używa nasza aplikacja, powinno mieć minimalne potrzebne uprawnienia. Np. jeśli aplikacja tylko odczytuje dane, użyjmy użytkownika z prawami SELECT (bez INSERT/UPDATE/DELETE/DROP). Jeśli używamy kilku baz, dajmy uprawnienia tylko do niezbędnych. W ten sposób, nawet jeśli dojdzie do ataku, szkody mogą być ograniczone brakiem uprawnień do destrukcyjnych operacji.
  • Bezpieczne przechowywanie danych w bazie: Choć to trochę obok tematu SQL injection, na bezpieczeństwo składa się też to, co trzymamy w bazie. Na przykład hasła użytkowników nigdy nie powinny być przechowywane w postaci czystej (plaintext). Zamiast tego należy je hashować z użyciem soli i bezpiecznego algorytmu, zanim zapiszemy w bazie. PHP oferuje funkcję password_hash() oraz password_verify(), które ułatwiają to zadanie – przykład podamy za chwilę w sekcji dobrych praktyk.
  • Filtrowanie danych wyjściowych: To już nie SQL injection, ale warto wspomnieć – jeśli wyświetlamy dane z bazy na stronie, należy je zabezpieczyć przed atakami XSS (Cross-Site Scripting) poprzez escapowanie w kontekście HTML (np. htmlspecialchars() w PHP). Przykładowo, jeśli ktoś wprowadzi w formularzu imię <script>alert('xss')</script> i zapiszemy to w bazie, to potem wyświetlając imię bez zabezpieczeń, wykonamy złośliwy skrypt na stronie. Dlatego każdą daną wyświetlaną w HTML powinniśmy przepuścić przez htmlspecialchars($dana, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). To nie dotyczy bezpośrednio połączenia z bazą, ale jest częścią ogólnego bezpieczeństwa aplikacji webowej.

Podsumowując: Nigdy nie ufaj danym wejściowym. Zabezpiecz zapytania SQL stosując parametry zamiast składania stringów, a dodatkowo waliduj dane po stronie aplikacji. Dzięki temu ryzyko udanego ataku SQL injection spada do zera, a Twoja baza i dane użytkowników pozostaną bezpieczne.

Dobre praktyki przy pracy z bazą danych w PHP

Na koniec omówmy kilka dobrych praktyk, które warto stosować, tworząc aplikacje PHP korzystające z baz danych. Dotyczą one zarówno kwestii technicznych (jak gospodarowanie zasobami połączenia), organizacji kodu, jak i bezpieczeństwa w szerszym ujęciu.

Zamykanie połączenia z bazą

W języku PHP połączenie z bazą danych zostanie automatycznie zamknięte po zakończeniu działania skryptu (np. po wysłaniu strony do przeglądarki)​. Oznacza to, że w prostej aplikacji internetowej zazwyczaj nie musimy ręcznie zamykać połączenia – PHP posprząta za nas. Niemniej jednak, są sytuacje, w których warto to zrobić samodzielnie:

  • Jeśli skrypt kończy pracę wcześniej (np. po obsłużeniu żądania AJAX) i wiemy, że połączenie nie będzie już potrzebne, można je zamknąć zanim zakończy się cały proces PHP. Zwolni to zasoby po stronie serwera bazy nieco szybciej.
  • W przypadku skryptów CLI (uruchamianych z linii poleceń) lub długotrwałych procesów, które mogą wykonywać wiele różnych zadań sekwencyjnie, warto zamykać połączenie gdy nie jest potrzebne, aby nie utrzymywać niepotrzebnie otwartego połączenia.

Jak zamknąć połączenie? W MySQLi (obiektowym) wywołujemy metodę $conn->close()​, w stylu proceduralnym funkcję mysqli_close($conn). W PDO nie ma dedykowanej metody, ale wystarczy usunąć obiekt PDO przez przypisanie go do null:

 $conn->close();    // MySQLi obiektowo
 // mysqli_close($conn); // MySQLi proceduralnie
 $pdo = null;       // PDO

Powyższe operacje zamkną połączenie natychmiast​. Warto też upewnić się, że zamykamy wszelkie otwarte obiekty wyników (result sets) czy kursory przed zamknięciem połączenia, choć w większości przypadków PHP to obsłuży.

Struktura i organizacja kodu

Dbanie o czytelność i modularność kodu jest ważne szczególnie w większych projektach. Kilka wskazówek:

  • Oddzielenie konfiguracji połączenia: Zamiast wpisywać na sztywno dane dostępowe (host, użytkownik, hasło, nazwa bazy) w wielu plikach, trzymajmy je w jednym miejscu. Może to być plik konfiguracyjny (np. PHP zwracający tablicę config) albo – bezpieczniej – plik poza repozytorium (jeśli kod jest wersjonowany publicznie). Często używa się też zmiennych środowiskowych (env) do przechowywania haseł. Dzięki temu łatwo zmienić np. hasło dostępu w jednym miejscu bez szukania w całym kodzie.
  • Reużywanie połączenia: Unikajmy tworzenia wielu połączeń do bazy w ramach jednego żądania, jeśli nie jest to potrzebne. Zazwyczaj wystarczy jedno połączenie na początku skryptu, które przekazujemy (np. przez zmienną globalną czy jako parametr do funkcji/obiektów). Otwarcie połączenia jest stosunkowo kosztowne, więc nie róbmy tego w pętli lub w wielu miejscach niezależnie.
  • Funkcje/klasy do operacji bazodanowych: Gdy nasz kod zaczyna intensywnie korzystać z bazy, warto wydzielić obsługę bazy danych do oddzielnych funkcji lub metod klas. Na przykład, zamiast wszędzie powtarzać zapytanie SELECT użytkownika po ID, można napisać funkcję getUserById($id) wewnątrz jakiejś klasy UserModel lub w oddzielnym pliku. Taka funkcja może wewnątrz korzystać z $pdo czy $conn, ale dla reszty kodu jest czarną skrzynką – reszta kodu woła tylko getUserById(5) i dostaje wynik, nie musząc znać szczegółów zapytania. To realizuje zasadę DRY (Don’t Repeat Yourself) i poprawia czytelność.
  • Rozdzielenie logiki aplikacji od logiki dostępu do danych: W profesjonalnych projektach często stosuje się architekturę warstwową (np. MVC – Model-View-Controller). Model odpowiada za operacje na danych (w tym na bazie), View za prezentację (HTML), a Controller za łączenie jednego z drugim. Starajmy się nie mieszać mocno zapytań SQL z kodem HTML generującym widok. Jeśli np. generujemy tabelkę HTML z listą produktów, to najpierw w jednym miejscu pobierzmy listę produktów z bazy (np. wywołując metodę modelu), a potem przekazujemy tę listę do części odpowiedzialnej za wyświetlanie. Takie rozdzielenie ułatwia utrzymanie kodu – zmiany w strukturze bazy wymagają zmiany tylko w warstwie modelu, a zmiany w wyglądzie tabelki tylko w warstwie widoku.
  • Nazewnictwo i czytelność: Nazywaj zmienne i funkcje w sposób zrozumiały. Zamiast $r = $c->query($q) lepiej $result = $conn->query($sql). Używaj komentarzy do wyjaśnienia nietrywialnych fragmentów. W przykładach stosowaliśmy komentarze dla czytelności (np. // Ustawienie trybu błędów na wyjątki).

Bezpieczne przechowywanie haseł

Kwestia przechowywania haseł dotyczy aplikacji, które mają system logowania/autoryzacji użytkowników (np. konta klientów sklepu internetowego). Nigdy nie przechowujemy haseł w postaci jawnej w bazie danych! Jeśli baza by wyciekła lub ktoś uzyskał do niej dostęp, nasze hasła użytkowników znalazłyby się na widelcu. Zamiast tego stosujemy funkcje skrótu (hash) – jednokierunkowego przekształcania hasła na inny ciąg znaków. PHP dostarcza gotowe, bezpieczne mechanizmy do haszowania haseł:

  • Funkcja password_hash($haslo, PASSWORD_DEFAULT) zwraca zaszyfrowany (ściślej: zahaszowany) odpowiednik podanego hasła, wykorzystując algorytm domyślny rekomendowany na dany moment (aktualnie jest to bcrypt, a w nowych wersjach PHP może to być Argon2). Funkcja automatycznie dodaje losową sól (salt) do każdego hasła, co zabezpiecza przed atakami słownikowymi i tęczowymi tablicami.
  • Funkcja password_verify($haslo_podane, $hash) pozwala sprawdzić, czy podane przez użytkownika hasło zgadza się z wcześniej zapisanym hashem.

Przykład użycia:

 <?php  // Rejestracja użytkownika – haszowanie hasła przed zapisaniem  $plainPassword = "MojeSekretneHaslo123";
  $hash = password_hash($plainPassword, PASSWORD_DEFAULT);
  // $hash zawiera teraz np. coś w stylu: $2y$10$92IXUNpkjO... (ciąg ~60 znaków)

  // Zapisz $hash do bazy zamiast oryginalnego hasła użytkownika

  // Logowanie – weryfikacja hasła
  $podaneHaslo = "MojeSekretneHaslo123";  // to zazwyczaj przyjdzie z formularza
  if (password_verify($podaneHaslo, $hash)) {
      echo "Hasło poprawne – użytkownik zalogowany.";
  } else {
      echo "Niepoprawne hasło.";
  }
 ?>

Dzięki temu nawet jeśli ktoś wykradnie naszą bazę, nie pozna właściwych haseł – zobaczy jedynie hashe, których złamanie (czyli odgadnięcie oryginalnego hasła) jest zaprojektowane jako ekstremalnie trudne i czasochłonne, szczególnie gdy używamy silnych algorytmów z soleniem​. Obecnie PASSWORD_DEFAULT wykorzystuje bcrypt, który jest wystarczający dla większości zastosowań, ale PHP ma też stałą PASSWORD_ARGON2ID pozwalającą użyć Argon2 (jeśli jest dostępny na serwerze), który jest uważany za bardzo bezpieczny. Dobrą praktyką jest także co pewien czas sprawdzać, czy algorytm domyślny się nie zmienił i ewentualnie rehashować stare hashe (PHP oferuje funkcję password_needs_rehash() do tego celu).

Podsumowując: przechowuj w bazie wyłącznie hashe haseł, nigdy haseł wprost​. Dotyczy to również innych wrażliwych danych – jeśli np. musisz przechować numer karty kredytowej, rozważ użycie szyfrowania kluczem, do którego klucz nie jest trzymany w tej samej bazie.

Rozdzielenie warstw logiki

Ten temat częściowo pokryliśmy w organizacji kodu, ale warto go podkreślić jako ogólną dobrą praktykę: rozdzielaj warstwę dostępu do danych od reszty aplikacji. Oznacza to:

  • Logika biznesowa (reguły działania aplikacji, np. „użytkownik może kupić produkt tylko jeśli jest w magazynie”) powinna być pisana niezależnie od tego, jak dokładnie pobierane są dane. Może używać metod interfejsu do bazy, ale nie powinna zawierać np. surowych zapytań SQL.
  • Warstwa dostępu do danych (DAO – Data Access Object, albo model w terminologii MVC) powinna udostępniać czyste funkcje/metody do pobierania lub modyfikowania danych. W jej implementacji będą zapytania SQL (lub wywołania ORM – Object-Relational Mapping, jeśli jest używany).
  • Warstwa prezentacji (widok) nie powinna wykonywać zapytań do bazy. Powinna dostać już przygotowane dane od kontrolera/modelu i tylko je wyświetlić. Dzięki temu łatwo np. zmienić źródło danych (np. przenieść część logiki do cache) bez zmiany kodu widoku.

Na poziomie początkującym i średniozaawansowanym nie zawsze stosuje się od razu pełen MVC, ale nawet w prostych skryptach warto trzymać się zasady: SQL w jednym miejscu, HTML w innym. Unikajmy pisanego „spaghetti”, gdzie w połowie pętli generującej tabelę HTML nagle robimy zapytanie do bazy. Lepiej najpierw pobrać wszystko do tablicy, a potem iterować po tablicy generując HTML.

Dzięki rozdzieleniu warstw kod staje się czytelniejszy, łatwiejszy do testowania i rozbudowy. Przykładowo, jeśli za rok postanowimy zmienić bazę danych na inny system, wystarczy zmodyfikować warstwę dostępu do danych (lub nawet tylko ciąg połączenia w przypadku użycia PDO i standardowego SQL), a reszta aplikacji pozostanie nietknięta.

Podsumowanie

Wykorzystanie baz danych w PHP jest fundamentem dla budowy nowoczesnych aplikacji webowych, które muszą przechowywać i pobierać informacje w sposób trwały. Poznaliśmy dwie główne metody realizacji tego zadania: MySQLi i PDO, z ich różnicami, zaletami i ograniczeniami. Nauczyliśmy się, jak nawiązać połączenie z bazą, wykonać podstawowe operacje (SELECT, INSERT, UPDATE, DELETE), a następnie ulepszyć nasz kod poprzez zastosowanie zapytań z parametrami (prepared statements) w celu zwiększenia bezpieczeństwa i elastyczności. Omówiliśmy również, jak ważna jest obsługa błędów – tak, by aplikacja była odporna na nieprzewidziane sytuacje – oraz jak krytyczne jest zabezpieczenie się przed atakami SQL injection poprzez właściwe praktyki programistyczne​.

Na koniec podkreśliliśmy znaczenie dobrych praktyk: od zamykania połączeń i poprawnego przechowywania haseł​, po przemyślaną strukturę kodu i podział odpowiedzialności w aplikacji. Stosowanie tych zasad od początku ułatwi rozwijanie projektów w przyszłości i zapobiegnie wielu błędom.

Mając tę wiedzę, początkujący i średniozaawansowani programiści PHP powinni czuć się pewniej w tworzeniu aplikacji wykorzystujących bazy danych. Kluczem jest praktyka – warto samodzielnie napisać prosty skrypt łączący się z bazą, wypróbować zapytania, a następnie stopniowo wprowadzać przygotowane zapytania i inne omówione usprawnienia. Z czasem powyższe elementy staną się naturalną częścią waszego stylu kodowania, co zaowocuje bezpieczniejszym i bardziej niezawodnym oprogramowaniem. Powodzenia w kodowaniu!

Programowanie obiektowe zadania

Zadanie 1: Klasa Book z metodą getSummary()

Stwórz klasę Book, która ma pola title, author, year. Dodaj metodę getSummary(), która zwraca tekst:

Tytuł (Autor, Rok)”


Kroki rozwiązania:

  1. Utwórz klasę Book
  2. Dodaj właściwości: title, author, year
  3. Zbuduj konstruktor
  4. Zaimplementuj metodę getSummary()
  5. Przetestuj tworząc obiekt i wyświetlając podsumowanie

Przykład Rozwiązania:

 class Book {
    public $title;
    public $author;
    public $year;

    public function __construct($title, $author, $year) {
        $this->title = $title;
        $this->author = $author;
        $this->year = $year;
    }

    public function getSummary() {
        return "{$this->title} ({$this->author}, {$this->year})";
    }
 }

 // Test
 $book = new Book("1984", "George Orwell", 1949);
 echo $book->getSummary(); // 1984 (George Orwell, 1949)


Zadanie 2: Klasa Rectangle z metodami getArea() i getPerimeter()

Treść:

Zbuduj klasę Rectangle, która przechowuje długość i szerokość prostokąta oraz liczy pole i obwód.


Kroki:

  1. Utwórz klasę Rectangle
  2. Dodaj pola width, height
  3. Napisz metody getArea() i getPerimeter()
  4. Przetestuj obiekt

Rozwiązanie:

 class Rectangle {
    public $width;
    public $height;

    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }

    public function getArea() {
        return $this->width * $this->height;
    }

    public function getPerimeter() {
        return 2 * ($this->width + $this->height);
    }
 }

 // Test
 $r = new Rectangle(5, 10);
 echo $r->getArea(); // 50
 echo $r->getPerimeter(); // 30


Zadanie 3: Dziedziczenie – Vehicle i Car

Treść:

Utwórz klasę bazową Vehicle z metodą getInfo(), a następnie klasę Car, która rozszerza Vehicle i dodaje pole brand.


Kroki:

  1. Utwórz klasę Vehicle z polem type i metodą getInfo()
  2. Stwórz klasę Car dziedziczącą po Vehicle
  3. Dodaj pole brand i przesłoń getInfo()
  4. Utwórz obiekt i przetestuj

Rozwiązanie:

 class Vehicle {
public $type;

public function __construct($type) {
$this->type = $type;
}

public function getInfo() {
return "Typ pojazdu: {$this->type}";
}
}

class Car extends Vehicle {
public $brand;

public function __construct($type, $brand) {
parent::__construct($type);
$this->brand = $brand;
}

public function getInfo() {
return "Samochód: {$this->brand} ({$this->type})";
}
}

// Test
$car = new Car("osobowy", "Toyota");
echo $car->getInfo(); // Samochód: Toyota (osobowy)



Zadanie 4: Licznik z inkrementacją

Treść:

Zbuduj klasę Counter, która ma metodę increment() i getValue(). Inicjalna wartość to 0.


Kroki:

  1. Utwórz klasę Counter
  2. Dodaj prywatne pole $value
  3. Stwórz metodę increment() zwiększającą wartość
  4. Dodaj getValue() zwracającą aktualną wartość

Rozwiązanie:

 class Counter {
    private $value = 0;

    public function increment() {
        $this->value++;
    }

    public function getValue() {
        return $this->value;
    }
 }

 // Test
 $c = new Counter();
 $c->increment();
 $c->increment();
 echo $c->getValue(); // 2

Zadanie 5: Magazyn produktów – tablica obiektów

Treść:

Utwórz klasę Product z nazwą i ceną. Następnie klasę Warehouse, która przechowuje tablicę produktów i metodę getTotalValue() – zwracającą sumę cen.


Kroki:

  1. Stwórz klasę Product z polami name, price
  2. Zbuduj klasę Warehouse z polem products (tablica)
  3. Dodaj metodę addProduct() do magazynu
  4. Dodaj metodę getTotalValue()
  5. Utwórz kilka produktów, dodaj do magazynu i oblicz sumę

Rozwiązanie:

 class Product {
    public $name;
    public $price;

    public function __construct($name, $price) {
        $this->name = $name;
        $this->price = $price;
    }
 }

 class Warehouse {
    private $products = [];

    public function addProduct(Product $product) {
        $this->products[] = $product;
    }

    public function getTotalValue() {
        $total = 0;
        foreach ($this->products as $product) {
            $total += $product->price;
        }
        return $total;
    }
 }

 // Test
 $p1 = new Product("Mysz", 50);
 $p2 = new Product("Klawiatura", 120);

 $w = new Warehouse();
 $w->addProduct($p1);
 $w->addProduct($p2);

 echo $w->getTotalValue(); // 170

Programowanie obiektowe w PHP (OOP)

Programowanie obiektowe (ang. Object-Oriented Programming, OOP) to popularny paradygmat programowania, w którym kod organizujemy wokół obiektów – czyli struktur łączących dane oraz funkcje je przetwarzające. Zamiast pisać pojedyncze funkcje operujące na danych rozproszonych po całym programie, w OOP grupujemy powiązane ze sobą dane i operacje w jedną jednostkę zwaną klasą. Klasa pełni rolę szablonu (planu) definiującego obiekt, a każdy obiekt jest konkretną instancją tej klasy – można go traktować jak egzemplarz utworzony na podstawie planu​

Taka organizacja ułatwia modelowanie realnych konceptów w kodzie: obiekt może odpowiadać rzeczywistemu bytowi (np. Samochód, Osoba, Produkt), posiadać określone cechy (dane) oraz wykonywać określone działania.

OOP przynosi wiele korzyści, zwłaszcza w większych projektach. Pozwala m.in. na enkapsulację (hermetyzację) danych – obiekty pilnują własnego stanu wewnętrznego i udostępniają do niego kontrolowany dostęp za pomocą metod. Ułatwia to również ponowne wykorzystanie kodu i rozbudowę projektu bez powielania tych samych fragmentów. Poniżej, krok po kroku, omówimy podstawowe pojęcia programowania obiektowego w PHP, takie jak klasy, obiekty, właściwości (atrybuty), metody, a także mechanizmy dziedziczenia oraz interfejsy. Wszystkie przykłady są zgodne ze składnią PHP 8 (i nowszych), co oznacza m.in. wykorzystanie deklaracji typów dla właściwości i parametrów metod.

Paradygmat obiektowy – Klasy i obiekty

Na początek wyjaśnijmy, czym są klasy i obiekty w kontekście OOP. Klasa to definicja pewnego typu obiektów, opisuje ona ich strukturę oraz zachowanie. Można powiedzieć, że klasa jest jak przepis lub plan: określa, jakie właściwości (czyli dane/cechy) oraz metody (czyli funkcje/zachowania) będą mieć obiekty utworzone na jej podstawie. Z kolei obiekt to konkretna instancja klasy, utworzona w trakcie działania programu. Jeżeli klasa jest przepisem, to obiekt jest „potrawą” powstałą według tego przepisu. Klasa jako abstrakcja istnieje w kodzie, a obiekt istnieje w czasie wykonywania programu (np. w pamięci komputera).

Przykład (poza kodem): Wyobraź sobie klasę Samochód – definicję, która mówi, że każdy samochód ma pewne właściwości, np. kolor, markę i prędkość maksymalną, oraz metody, np. jazda() czy hamuj(). Samochód opisany przez klasę to koncept, natomiast obiekt typu Samochód to już konkretny samochód – np. czerwony Ford Mustang, który ma ustawiony kolor na czerwony, markę na Ford, i może wykonać metodę jazda() (czyli np. ruszyć). Możemy utworzyć wiele obiektów na bazie jednej klasy (np. różne samochody o innych parametrach), podobnie jak według jednego planu architektonicznego można zbudować wiele domów.

Podsumowując: klasa definiuje strukturę i zachowanie, a obiekt tę definicję urzeczywistnia jako pojedynczy byt z własnym stanem. Klasa jest pewnym typem danych, który definiujemy sami, a obiekty są wartościami/zmiennymi tego typu.

Tworzenie klas w PHP – składnia, właściwości i metody

Skoro wiemy, czym jest klasa, zobaczmy jak zdefiniować własną klasę w PHP. Podstawowa składnia jest prosta: używamy słowa kluczowego class i nazwy klasy, a jej zawartość umieszczamy w nawiasach klamrowych { }. Wewnątrz klasy definiujemy właściwości (pola) oraz metody (funkcje należące do klasy). Na przykładzie omówimy te elementy:

  • Właściwości – to zmienne przechowujące stan obiektu. Każda instancja (obiekt) klasy ma własne kopie tych zmiennych. W PHP definiujemy właściwości podobnie jak zwykłe zmienne, poprzedzając je deklaracją dostępu (np. public) i opcjonalnie określając typ danych oraz wartość domyślną.
  • Metody – to funkcje zdefiniowane wewnątrz klasy, które opisują zachowanie obiektu. Metody mogą odczytywać lub modyfikować właściwości (stan) obiektu, wykonywać obliczenia, zwracać wartości, itp. Technicznie definiujemy je jak zwykłe funkcje, ale również poprzedzamy deklaracją dostępu (np. public). W ciele metody możemy odwoływać się do właściwości danej instancji poprzez specjalną zmienną $this (która reprezentuje „bieżący” obiekt).
  • Konstruktor – to specjalna metoda o nazwie __construct, która automatycznie wykonuje się w momencie tworzenia nowego obiektu. Służy do początkowej inicjalizacji obiektu (np. ustawienia wartości początkowych właściwości). Konstruktor definiujemy jak zwykłą metodę, ale PHP wywoła go za nas przy użyciu new (o tym za moment). Jeśli nie zdefiniujemy konstruktora, PHP utworzy obiekt bez dodatkowych kroków inicjalizacji, a właściwości można ustawiać „ręcznie” po utworzeniu obiektu.

Przy definiowaniu właściwości i metod używamy tzw. modyfikatorów dostępu, które określają, z jakiego miejsca w programie można się do nich odwołać:

  • public – właściwość lub metoda publiczna jest dostępna wszędzie (zarówno z zewnątrz obiektu, jak i wewnątrz klasy oraz w klasach dziedziczących).
  • private – element prywatny jest dostępny tylko wewnątrz tej samej klasy. Nie można go odczytać ani zmienić bezpośrednio poza tą klasą (ani w obiektach, ani w podklasach). Służy to ukrywaniu wewnętrznej reprezentacji obiektu przed światem zewnętrznym.
  • protected – element chroniony jest dostępny wewnątrz klasy oraz w jej podklasach (dziedziczących), ale nie z zewnątrz (nie można go użyć na obiekcie spoza definicji klasy). Jest to przydatne, gdy chcemy dopuścić użycie np. właściwości w klasach dziedziczących, ale nadal ukryć ją przed resztą programu.

Poniżej zdefiniujemy prostą klasę o nazwie Person (Osoba), która posłuży za przykład. Klasa ta ma dwie właściwości (imię oraz wiek osoby) oraz jedną metodę. Zaimplementujemy także konstruktor, aby wygodnie ustawiać te właściwości przy tworzeniu obiektu:

 class Person {

    public string $name;   // właściwość publiczna przechowująca imię

    public int $age;       // właściwość publiczna przechowująca wiek

    // Konstruktor wywoływany przy tworzeniu nowego obiektu:

    public function __construct(string $name, int $age) {

        // Słowo $this oznacza "ten konkretny obiekt". Używamy go, aby

        // odwołać się do właściwości obiektu i ustawić je:

        $this->name = $name;

        $this->age = $age;

    }

    // Przykładowa metoda obiektu:

    public function sayHello(): void {

        echo "Cześć, mam na imię $this->name.";

    }

 }

Powyżej utworzyliśmy klasę Person ze wszystkimi wymaganymi elementami:

  1. Zaczyna się od deklaracji class Person { … }.
  2. Wewnątrz zadeklarowaliśmy dwie właściwości ($name i $age) poprzedzone modyfikatorem public i oznaczeniem typów (string i int). To oznacza, że każdy obiekt Person będzie przechowywał własne name i age, dostępne publicznie.
  3. Zdefiniowaliśmy konstruktor __construct, który przyjmuje dwa parametry (też ze zdefiniowanymi typami) i ustawia właściwości obiektu na przekazane wartości. Dzięki temu od razu przy tworzeniu obiektu będziemy mogli podać imię i wiek.
  4. Dodaliśmy metodę sayHello(), która wypisuje przywitanie z imieniem. Zauważ, że korzysta ona z $this->name, aby odczytać właściwość obiektu.

Uwaga – skrócona inicjalizacja właściwości (PHP 8)

W PHP 8 wprowadzono wygodną skróconą składnię inicjalizowania właściwości w konstruktorze (tzw. constructor property promotion). Pozwala ona zadeklarować właściwości bezpośrednio w definicji parametrów konstruktora, co upraszcza kod. Powyższą klasę Person moglibyśmy zdefiniować równoważnie krócej:

 class Person {

    public function __construct(

        public string $name,

        public int $age

    ) {

        // ciało może być puste, PHP sam przypisze przekazane wartości

        // do odpowiednich właściwości na podstawie powyższej deklaracji

    }

    public function sayHello(): void {

        echo "Cześć, mam na imię $this->name.";

    }

 }

Takie zapisy skracają kod, ale mechanicznie działają tak samo jak poprzednia definicja – tworzą publiczne właściwości $name i $age oraz przypisują im wartości przekazane do konstruktora. W dalszych przykładach będziemy już używać zwykłej, pełnej formy dla przejrzystości, jednak warto wiedzieć o istnieniu tej nowej składni.

Instancjonowanie obiektów i korzystanie z metod i właściwości

Mając zdefiniowaną klasę, możemy teraz utworzyć obiekt tej klasy, czyli tzw. zainstancjować klasę. W PHP dokonujemy tego za pomocą operatora new. Wywołanie new NazwaKlasy() powoduje utworzenie nowego obiektu danego typu. Jeśli klasa posiada konstruktor, w nawiasach podajemy argumenty, które zostaną przekazane do __construct.

Kontynuując nasz przykład, utwórzmy dwa obiekty klasy Person i zobaczmy, jak korzystać z ich właściwości i metod:

 $person1 = new Person("Jan", 20);       // tworzymy obiekt Person, imię="Jan", wiek=20

 $person2 = new Person("Anna", 22);      // tworzymy inny obiekt Person, imię="Anna", wiek=22

 // Dostęp do właściwości obiektu za pomocą operatora "->":

 echo $person1->name;    // wyświetli: Jan

 $person2->age = 23;     // zmieniamy wartość właściwości age obiektu person2 (Anna ma teraz 23 lata)

 // Wywoływanie metody obiektu:

 $person1->sayHello();   // wypisze: Cześć, mam na imię Jan.

 $person2->sayHello();   // wypisze: Cześć, mam na imię Anna.

W powyższym kodzie widać kilka ważnych rzeczy:

  • Obiekty tworzymy poprzez new Klasa(…). W naszym przypadku new Person(„Jan”, 20) wywołuje konstruktor klasy Person z argumentami „Jan” i 20, co skutkuje utworzeniem obiektu z ustawionym name = „Jan” i age = 20. Zmienna $person1 przechowuje referencję do tego obiektu. Analogicznie tworzymy $person2.
  • Do właściwości obiektu odwołujemy się przez składnię $obiekt->właściwość. Np. $person1->name zwraca wartość właściwości name obiektu $person1. Możemy też przypisywać wartości: $person2->age = 23 zmienia wiek drugiej osoby. (Uwaga: w naszym przypadku właściwości są publiczne, więc mamy do nich dostęp bezpośrednio. Gdyby były prywatne, taka operacja spoza klasy byłaby niedozwolona i wymagałaby użycia metod get/set – na początek jednak trzymamy się publicznych dla prostoty).
  • Metody obiektu wywołujemy podobnie, używając $obiekt->nazwaMetody(…). Przykładowo, $person1->sayHello() wykonuje metodę sayHello() na obiekcie $person1. W efekcie zostanie wypisany komunikat zawierający imię tej osoby. Każdy obiekt zachowuje się zgodnie ze swoją klasą – wywołanie sayHello() na $person2 działa, choć $person2 to inna instancja, bo klasa Person definiuje tę metodę i obie instancje ją odziedziczyły.

Warto zauważyć, że każdy obiekt ma własny stan – w naszym przykładzie $person1->name to „Jan”, a $person2->name to „Anna”. Obiekty te są niezależne, zmiana właściwości w jednym nie wpływa na drugi. OOP pozwala więc tworzyć wiele instancji z jednego schematu (klasy), każda z własnymi danymi.

Tip: Obiekt w PHP jest przekazywany przez referencję, co oznacza, że jeśli przekażemy obiekt do funkcji lub przypiszemy do innej zmiennej, nadal będzie to ten sam obiekt (dwie zmienne mogą referować ten sam obiekt). Dla początkujących wystarczy jednak pamiętać, że zmienna z obiektem to coś więcej niż prosta wartość – zawiera ona dostęp do struktury złożonej (danych i funkcji).

Dziedziczenie – klasy bazowe i pochodne

Jedną z najważniejszych cech paradygmatu obiektowego jest dziedziczenie. Dziedziczenie pozwala tworzyć nową klasę w oparciu o już istniejącą klasę, przejmując jej właściwości i metody. Nowa klasa nazywa się klasą pochodną (podklasą), zaś klasa oryginalna to klasa bazowa (lub rodzic). W PHP do wskazania dziedziczenia używamy słowa kluczowego extends przy definicji klasy pochodnej.

Dzięki dziedziczeniu możemy kod wspólny umieścić w klasie bazowej, a klasy pochodne rozszerzają jej funkcjonalność (dodają nowe właściwości/metody lub zmieniają te odziedziczone). Jest to przydatne przy modelowaniu hierarchii obiektów – np. klasa bazowa Osoba może zawierać cechy wspólne wszystkim osobom, a klasy pochodne Student czy Nauczyciel dodadzą specyficzne atrybuty lub zachowania.

Jak to działa? Klasa dziedzicząca automatycznie otrzymuje wszystkie publiczne i chronione właściwości oraz metody z klasy bazowej​

. Nie musi ich ponownie definiować – są one dostępne jak część definicji podklasy. To oznacza, że np. jeśli Student dziedziczy po Person, to Student ma już właściwości $name i $age oraz metodę sayHello() tak jak Person. Klasa pochodna może jednak nadpisać wybrane metody klasy bazowej, definiując własną implementację o tej samej nazwie (i sygnaturze). Może też dodać nowe właściwości i metody, których nie ma w klasie bazowej.

Uwaga: Klasa pochodna nie ma dostępu do elementów zadeklarowanych jako private w klasie bazowej (są one ukryte). Dostaje tylko to, co jest public lub protected. Dlatego, jeśli chcemy by podklasy mogły korzystać z pewnej właściwości/metody, a równocześnie by była ona niewidoczna na zewnątrz obiektów, powinniśmy użyć modyfikatora protected w klasie bazowej.

Stwórzmy teraz klasę Student dziedziczącą po naszej klasie Person. Student będzie mieć te same właściwości co Person (imię, wiek) oraz dodamy mu nową właściwość major (kierunek studiów). Zaimplementujemy też własną wersję metody powitania, która uwzględni dodatkową informację:

 class Student extends Person {

    public string $major;  // nowa właściwość dla Student (np. kierunek studiów)

    // Konstruktor Student przyjmuje także kierunek studiów.

    // Wykorzystujemy konstruktor Person do ustawienia imienia i wieku:

    public function __construct(string $name, int $age, string $major) {

        parent::__construct($name, $age);   // wywołanie konstruktora klasy bazowej Person

        $this->major = $major;             // ustawienie nowej właściwości

    }

    // Nadpisujemy metodę sayHello odziedziczoną z Person:

    public function sayHello(): void {

        echo "Cześć, mam na imię $this->name i studiuję $this->major.";

    }

 }

Zwróć uwagę na kilka rzeczy w tej definicji:

  • Użyliśmy extends Person – to oznacza, że Student dziedziczy po klasie Person. Dzięki temu Student automatycznie ma właściwości $name, $age oraz metodę sayHello() (choć tę akurat zaraz nadpisujemy).
  • Dodaliśmy nową właściwość $major (kierunek). Student ma więc wszystko to, co Person, plus $major.
  • Zdefiniowaliśmy konstruktor w klasie Student przyjmujący trzy parametry. Wewnątrz konstruktora wywołujemy parent::__construct($name, $age). Słowo kluczowe parent odnosi się do klasy bazowej, a wywołanie parent::__construct(…) uruchamia konstruktor z klasy Person. To pozwala nam wykorzystać logikę inicjalizacji z bazowej klasy (ustawienie imienia i wieku), a następnie dopisać własną (ustawienie $major). Gdybyśmy tego nie zrobili, musielibyśmy sami ustawić $this->name i $this->age – co oznacza powielenie kodu już istniejącego w Person. Wywołanie konstruktora bazowego eliminuje duplikację.
  • Nadpisaliśmy metodę sayHello(). W klasie Person ta metoda wypisuje komunikat z imieniem; w klasie Student chcemy, by zawierała też informację o kierunku studiów. Dlatego definiujemy metodę o identycznej sygnaturze (sayHello(): void) w klasie pochodnej. Ta definicja zastąpi odziedziczoną wersję. Teraz dla obiektów Student wywołanie sayHello() będzie wykonywać nowy kod (zawierający także $this->major). W naszym przypadku nie użyliśmy parent::sayHello(), lecz mogliśmy – np. moglibyśmy najpierw wywołać kod z Person, a potem dopisać coś od siebie. Jeśli jednak całkowicie zmieniamy działanie metody, nie ma takiej potrzeby.

Teraz użyjmy klasy Student w praktyce:

 $student = new Student("Jan", 20, "Informatyka");  // tworzymy obiekt Student (imię, wiek, kierunek)

 $student->sayHello();  // wypisze: Cześć, mam na imię Jan i studiuję Informatyka.

 echo $student->age;    // odziedziczona właściwość age, wynik: 20

W powyższym kodzie widać, że obiekt klasy Student zachowuje się podobnie do obiektu Person, ale ma dodatkowe cechy. Wywołanie $student->sayHello() skorzystało z nadpisanej metody i wyświetliło komunikat uwzględniający kierunek studiów. Właściwość $student->age jest dostępna tak samo jak w Person (odziedziczona jako publiczna). Gdybyśmy wywołali $student->sayHello() bez nadpisywania tej metody, zostałaby wykonana wersja z klasy bazowej Person (odziedziczona, bo każda klasa dziedziczy publiczne/protected metody bazowe, dopóki ich nie nadpisze).

Dziedziczenie podsumowując: Pozwala tworzyć hierarchię klas od ogólnych do szczegółowych. Klasa pochodna rozszerza (ang. extends) klasę bazową, co sprzyja ponownemu użyciu kodu i organizacji. W PHP każda klasa może dziedziczyć tylko po jednej innej klasie (brak wielokrotnego dziedziczenia klas). Jeśli potrzebujemy rozdzielić wspólne cechy między różne gałęzie hierarchii, do akcji wkraczają interfejsy, które omówimy za chwilę.

Interfejsy – definicja i implementacja

Oprócz dziedziczenia klas, PHP oferuje mechanizm interfejsów. Interfejs można rozumieć jako zbiór metod (sygnatur metod) bez ich implementacji. Definiując interfejs, określamy co dana klasa musi robić (jakie metody udostępniać), ale nie jak to robi. Następnie różne klasy mogą implementować ten interfejs – czyli zobowiązać się do napisania tych konkretnych metod. Dzięki interfejsom możemy zaprojektować kod w formie kontraktów: jeśli klasa implementuje interfejs, to gwarantuje, że pewne metody w niej istnieją, co umożliwia traktowanie różnych obiektów w ten sam sposób, o ile spełniają ten kontrakt.

Interfejsy są przydatne, gdy chcemy zapewnić wspólne metody dla różnych, niezależnych klas. Pozwalają określić, jakie metody klasa powinna zaimplementować

, i ułatwiają używanie różnych klas w taki sam sposób. Innymi słowy, jeśli kilka klas implementuje ten sam interfejs, ich obiekty można będzie wykorzystać wymiennie tam, gdzie oczekujemy obiektu „mającego” dany interfejs – nawet jeśli te klasy nie są ze sobą powiązane przez dziedziczenie. (Jest to forma realizacji polimorfizmu, choć nie zagłębiamy się tutaj w to pojęcie.)

Definiowanie interfejsu: W PHP interfejs definiujemy podobnie jak klasę, z tą różnicą, że używamy słowa kluczowego interface zamiast class. Wewnątrz interfejsu umieszczamy nagłówki metod bez implementacji (czyli same deklaracje, zakończone średnikiem ;, bez ciała). Nie definiujemy w interfejsie właściwości (można jedynie stałe). Wszystkie metody zadeklarowane w interfejsie są domyślnie publiczne (nie podajemy modyfikatora dostępu – inny i tak nie jest dozwolony).

Implementacja interfejsu w klasie: Klasa implementuje interfejs używając słowa kluczowego implements w swojej definicji. Może implementować wiele interfejsów (oddzielamy ich nazwy przecinkami). Taka klasa musi zdefiniować wszystkie metody zadeklarowane w interfejsie (z dokładnie takimi samymi sygnaturami). Jeśli którejś zabraknie lub różni się sygnaturą, PHP zgłosi błąd. W jednej klasie możemy jednocześnie dziedziczyć po innej klasie (extends) i implementować interfejs(y) (implements). Implementacja interfejsu to po prostu zapewnienie ciał tych metod w klasie.

Przyjrzyjmy się przykładom, by to wyjaśnić. Zdefiniujmy interfejs Instrument zawierający metodę play() (graj). Następnie stworzymy dwie klasy: Gitara oraz Perkusja, które ten interfejs zaimplementują na różne sposoby:

 interface Instrument {

    public function play(): void;  // każda klasa implementująca musi mieć tę metodę

 }

 class Gitara implements Instrument {

    public function play(): void {

        echo "Gitara: brzdęk!";

    }

 }

 class Perkusja implements Instrument {

    public function play(): void {

        echo "Perkusja: bum!";

    }

 }

Wyjaśnienie:

  • Interfejs Instrument definiuje jedną metodę play(). Nie ma ciała metody – to tylko „obietnica”, że klasa, która zaimplementuje ten interfejs, dostarczy własny kod tej metody.
  • Klasa Gitara deklaruje implements Instrument, co zobowiązuje ją do napisania metody play(). Widzimy implementację: wypisuje tekst „Gitara: brzdęk!”.
  • Klasa Perkusja również implementuje Instrument i zapewnia własną wersję metody play() – w tym przypadku wypisując „Perkusja: bum!”.
  • Obie klasy mogą oczywiście mieć też inne metody czy właściwości (specyficzne dla siebie), ale kluczowe jest to, że spełniają kontrakt narzucony przez Instrument – czyli mają metodę play().

Teraz zobaczmy użycie takiego interfejsu w praktyce:

 $guitar = new Gitara();

 $drums = new Perkusja();

 $guitar->play();   // wyświetli: Gitara: brzdęk!

 $drums->play();    // wyświetli: Perkusja: bum!

Na pierwszy rzut oka może się to nie różnić od zwykłych klas – po prostu wywołujemy metodę na obiekcie danej klasy. Siła interfejsów ujawnia się jednak, gdy zauważymy, że nie musimy wiedzieć, z jaką konkretnie klasą mamy do czynienia, by móc wywołać play(). Wystarczy nam informacja, że obiekt implementuje interfejs Instrument. Możemy np. napisać funkcję:

 function zagrajKoncert(Instrument $instr) {

    // parametr $instr może być dowolnym obiektem implementującym Instrument

    $instr->play();

 }

Funkcja zagrajKoncert(Instrument $instr) przyjmie dowolny obiekt typu Instrument (czyli dowolny, który implementuje ten interfejs). W jej ciele wywołujemy $instr->play(), a dzięki interfejsowi PHP zapewnia, że taka metoda na pewno istnieje w obiekcie. W efekcie możemy przekazać do tej funkcji zarówno new Gitara(), jak i new Perkusja(), a nawet obiekty innych klas implementujących Instrument (gdyby istniały), i za każdym razem zadziała poprawnie wywołując odpowiednią implementację play(). To samo tyczy się np. przechowywania obiektów różnych klas we wspólnej tablicy Instrumentów i iterowania po nich.

Po co są interfejsy? Uogólniając, interfejsy pozwalają definiować role lub zachowania, które różne obiekty mogą przyjąć. Przykładowo interfejs JsonSerializable (wbudowany w PHP) wymaga metody jsonSerialize() – różne klasy mogą go implementować, aby ich obiekty dało się zamienić na JSON. Interfejsy przydają się też jako zamiennik wielokrotnego dziedziczenia: w PHP klasa może dziedziczyć tylko po jednej klasie, ale może implementować wiele interfejsów, więc może spełniać wiele „kontraktów”. Ułatwiają również pisanie czystego, modularnego kodu – można najpierw zdefiniować interfejsy (API), a potem tworzyć różne realizacje (klasy) tego API.

Warto pamiętać, że interfejsu nie da się instancjonować – nie możemy zrobić new Instrument(), bo to nie klasa z pełną implementacją, a jedynie zbiór metod do zaimplementowania. Interfejs służy wyłącznie jako opis wymagań dla klas.

Podsumowanie

  • Klasa w PHP to definicja obiektu: określa zestaw właściwości (danych) i metod (funkcji) wspólnych dla wszystkich obiektów tego typu. Klasa jest jak szablon lub przepis, na podstawie którego tworzymy obiekty.
  • Obiekt (instancja klasy) to konkretny egzemplarz utworzonej klasy, posiadający własny stan. Obiekty tworzymy za pomocą new Klasa(…). Możemy utworzyć wiele obiektów z jednej klasy, każdy z własnymi wartościami właściwości.
  • Właściwości i metody definiujemy wewnątrz klasy. Właściwości przechowują stan obiektu, metody definiują jego zachowanie. Modyfikatory dostępu (public/private/protected) pozwalają kontrolować, kto ma dostęp do tych składowych (np. tylko sama klasa czy także jej podklasy, czy cały program).
  • Konstruktor (__construct) to metoda uruchamiana automatycznie przy tworzeniu obiektu (new), służąca do inicjalizacji obiektu (np. ustawienia początkowych wartości właściwości).
  • Dziedziczenie (extends) umożliwia tworzenie nowych klas na bazie już istniejących. Klasa pochodna dziedziczy publiczne i chronione właściwości/metody klasy bazowej​

, dzięki czemu możemy ponownie wykorzystać kod. Podklasa może dodawać własne elementy oraz nadpisywać metody bazowe, aby zmienić lub rozszerzyć ich działanie. PHP wspiera dziedziczenie pojedyncze (jedna klasa bazowa).

  • Interfejsy (interface … implements) pozwalają definiować zestaw metod bez implementacji – swego rodzaju kontrakt. Klasy implementujące interfejs muszą dostarczyć kod tych metod. Interfejsy umożliwiają stosowanie wspólnego zestawu metod w różnych klasach (nawet niezwiązanych dziedziczeniem) i traktowanie obiektów tych klas jednolicie, poprzez typ interfejsu​

. Klasa może implementować wiele interfejsów, dzięki czemu łączy różne role. Interfejsów nie można instancjonować (nie tworzymy obiektów interfejsu bezpośrednio).

OOP to potężny paradygmat – na początku wymaga zrozumienia nowych pojęć, ale w zamian oferuje przejrzystą organizację kodu i skalowalność.

Obsługa formularzy w PHP – zadania

Zadanie 1: Formularz kontaktowy z walidacją danych

Opis sytuacji:
Wyobraź sobie prostą stronę internetową, na której potrzebny jest formularz kontaktowy. Użytkownik powinien móc wpisać swoje imię, adres e-mail oraz treść wiadomości, a następnie wysłać tę wiadomość do właściciela strony. Należy upewnić się, że dane wpisane przez użytkownika są poprawne – np. adres e-mail ma prawidłowy format – zanim przetworzymy (np. wyświetlimy lub wyślemy) tę wiadomość.

Wymagania funkcjonalne:

  • Pole Imię i nazwisko – wymagane pole tekstowe (nie może być puste).
  • Pole Adres e-mail – wymagane pole tekstowe (najlepiej typu „email”); powinno zawierać poprawnie sformatowany adres e-mail.
  • Pole Wiadomość – wymagane pole typu textarea; minimalna długość np. 10 znaków (aby wiadomość nie była za krótka).
  • Opcja Zapisz do newslettera – opcjonalny checkbox (pole wyboru); jeśli zaznaczone, użytkownik wyraża chęć zapisu do newslettera.
  • Walidacja: Formularz powinien zostać przetworzony tylko, jeśli wszystkie wymagane pola zostały wypełnione poprawnie. W przeciwnym razie należy wyświetlić komunikaty o błędach i nie „wysyłać” wiadomości.
  • Po pomyślnej walidacji wyświetlane jest podziękowanie lub podsumowanie wysłanej wiadomości.

Kroki rozwiązania:

  1. Stworzenie formularza HTML: Utwórz formularz HTML metodą POST, zawierający pola: tekstowe dla imienia, tekstowe (typu email) dla adresu e-mail, pole tekstowe wielowierszowe <textarea> dla wiadomości oraz checkbox dla newslettera. Dodaj przycisk typu submit do wysłania formularza.
  2. Sprawdzenie przesłania formularza: Po stronie PHP sprawdź, czy formularz został przesłany. Można to zrobić np. przez sprawdzenie $_SERVER[„REQUEST_METHOD”] == „POST” lub obecności konkretnego pola (np. isset($_POST[’email’])).
  3. Pobranie i walidacja danych: Pobierz dane z pól formularza z tablicy $_POST. Następnie zweryfikuj:
    • Czy imię nie jest puste (ewentualnie czy ma określoną minimalną długość).
    • Czy adres e-mail nie jest pusty oraz czy ma poprawny format. Do sprawdzenia formatu można użyć wbudowanej funkcji PHP filter_var($email, FILTER_VALIDATE_EMAIL) – jest to najprostszy i bezpieczny sposób weryfikacji poprawności adresu e-mail​
  1. Ponowne wyświetlenie formularza z błędami (jeśli wystąpiły): Jeśli lista błędów nie jest pusta, wyświetl ponownie formularz HTML wraz z komunikatami. Dobrą praktyką jest zachowanie wcześniej wprowadzonych przez użytkownika danych w polach (tzw. sticky form), aby nie musiał wpisywać wszystkiego od nowa. Można to osiągnąć poprzez ustawienie atrybutu value (dla <input>) lub zawartości <textarea> na poprzednie wartości z $_POST.
  2. Przetworzenie danych po poprawnej walidacji: Jeżeli wszystkie wymagane pola są poprawnie wypełnione, wykonaj odpowiednią akcję. W tym przypadku zamiast faktycznego wysyłania e-maila (co wykracza poza zakres zadania), możesz po prostu wyświetlić podsumowanie lub komunikat potwierdzający, np. „Dziękujemy [imię], Twoja wiadomość została wysłana.”. Pamiętaj, aby wyświetlane dane użytkownika odpowiednio oczyszczać (np. funkcją htmlspecialchars), żeby uniknąć wstrzyknięcia niepożądanego kodu.

Dane wejściowe:

 $name = $email = $message = "";

 $subscribe = false;

 $errors = [];

Zadanie 2: Formularz zamówienia produktu z kalkulacją ceny

Opis sytuacji:
Napisz formularz zamówienia produktu (np. koszulki), w którym użytkownik może wybrać różne opcje zamawianego towaru. Na podstawie wyborów użytkownika obliczana jest cena zamówienia. Przykładowo, koszulka może mieć różne rozmiary (wplywające na cenę), użytkownik może wybrać rodzaj dostawy (np. standardowa lub ekspresowa) oraz określić ilość sztuk. Dodatkowo może zaznaczyć opcję dodatkową, taką jak pakowanie na prezent. Po wysłaniu formularza powinniśmy podsumować zamówienie: wypisać wybrane opcje oraz obliczyć łączny koszt.

Wymagania funkcjonalne:

  • Pole Rozmiar koszulki – wybór z listy rozwijanej (select) spośród np. S, M, L. Każdy rozmiar ma ustaloną cenę bazową (np. S – 20 zł, M – 25 zł, L – 30 zł).
  • Pole Ilość – pole numeryczne (typ number); określa liczbę sztuk. Wymagane, musi być co najmniej 1 (i np. maksymalnie 10).
  • Pole Dostawa – wybór jednej z opcji dostawy (radio). Dostępne opcje: „Standardowa” (np. 0 zł) lub „Ekspresowa” (np. +10 zł dopłaty). Jedna z nich musi być wybrana (domyślnie można zaznaczyć Standardową).
  • Pole Pakowanie na prezent – opcjonalny checkbox; jeśli zaznaczony, doliczana jest opłata (np. +5 zł).
  • (Opcjonalnie) Uwagi do zamówienia – pole tekstowe (textarea) na dodatkowe informacje od klienta dotyczące zamówienia (nieobowiązkowe, bez specjalnej walidacji poza ewentualnym limitem długości).
  • Walidacja: Sprawdź, czy wymagane pola zostały wypełnione/poprawnie wybrane:
    • Rozmiar – czy został wybrany (lista rozwijana może mieć domyślną opcję typu „Wybierz…”, wtedy trzeba sprawdzić, czy użytkownik zmienił wartość).
    • Ilość – czy jest podana i czy jest liczbą z dozwolonego zakresu (>=1, <=10).
    • Dostawa – czy jedna z opcji została wybrana (jeśli nie ustawimy domyślnej w HTML).
  • Po wysłaniu i poprawnej walidacji wyświetl podsumowanie: wybrany rozmiar, ilość, typ dostawy, informacja o pakowaniu na prezent oraz oblicz łączną cenę zamówienia według podanych zasad cenowych.

Kroki rozwiązania:

  1. Stworzenie formularza HTML: Utwórz formularz (method=”post”). Dodaj pole <select name=”size”> z opcjami dla rozmiaru (np. „S”, „M”, „L” – pierwszą opcją może być domyślne „Wybierz rozmiar…” z pustą wartością). Dodaj pole <input type=”number” name=”quantity”> dla ilości (możesz ustawić min=”1″, max=”10″ dla podpowiedzi zakresu). Utwórz grupę przycisków <input type=”radio” name=”shipping”> dla dostawy (np. value=”standard” i „express”). Dodaj checkbox <input type=”checkbox” name=”gift”> dla opcji prezent. Na koniec dodaj przycisk submit.
  2. Sprawdzenie przesłania i pobranie danych: W skrypcie PHP sprawdź $_SERVER[„REQUEST_METHOD”]. Jeśli to POST, pobierz wartości z $_POST: rozmiar, ilość (pamiętając, że będzie jako string – można rzutować na int lub użyć filter_var), rodzaj dostawy, status checkboxa prezent.
  3. Walidacja danych wejściowych:
    • Sprawdź wymagane pola: rozmiar (czy nie jest pusty lub domyślny), ilość (czy nie pusta i czy jest liczbą w wymaganym przedziale), dostawa (czy jest ustawiona).
    • Jeśli quantity ma być liczbą całkowitą, można użyć filter_var($_POST[’quantity’], FILTER_VALIDATE_INT) z opcjami min/max lub funkcji ctype_digit/is_numeric i rzutowania, by upewnić się, że to poprawna liczba.
    • W razie błędów (np. brak wyboru rozmiaru, zbyt duża/mała ilość, brak wyboru dostawy) dodaj odpowiednie komunikaty do listy błędów.
  4. Obsługa błędów: Jeśli lista błędów nie jest pusta, wyświetl formularz ponownie z komunikatami. Podobnie jak w zadaniu 1, wypełnij pola uprzednio podanymi wartościami (value, checked, selected) – np. poprzez porównanie pobranej wartości z wartością pola w kodzie HTML.
  5. Obliczenie ceny i wyświetlenie podsumowania: Jeśli nie ma błędów, oblicz cenę:
    • Ustal cenę bazową na podstawie rozmiaru (np. za pomocą tablicy asocjacyjnej: [’S’=>20, 'M’=>25, 'L’=>30]).
    • Pomnóż cenę bazową przez ilość sztuk.
    • Dodaj koszt dostawy (0 lub 10 w zależności od wybranej opcji).
    • Jeśli zaznaczono pakowanie na prezent, dodaj np. 5 zł.
    • Wynik zapisz jako łączną kwotę. Następnie wyświetl czytelne podsumowanie, np.: „Zamówiłeś X koszulek w rozmiarze Y. Dostawa: Z. Pakowanie na prezent: tak/nie. Razem do zapłaty: N zł.„.

Dane wejściowe:

 $size = $shipping = "";

 $quantity = 1;

 $gift = false;

 $errors = [];

 // Cennik dla rozmiarów

 $prices = [

     "S" => 20,

     "M" => 25,

     "L" => 30

 ];

Zadanie 3: Formularz wyboru wielu opcji (checkboxy)

Opis sytuacji:
Stwórz formularz, w którym użytkownik może zaznaczyć wiele opcji jednocześnie, a aplikacja prawidłowo je odbierze i przetworzy. Przykładowo, załóżmy, że użytkownik wypełnia ankietę zainteresowań i może wybrać dowolną liczbę kategorii, które go interesują (np. Technologia, Sport, Muzyka, Podróże, itd.). Po wysłaniu formularza wybrane kategorie zostaną wypisane. Ważne jest pokazanie, jak obsłużyć w PHP dane z grupy checkboxów (w postaci tablicy).

Wymagania funkcjonalne:

  • Lista kategorii (np. zainteresowań) przedstawiona jako zestaw pól checkbox (wielokrotny wybór). Przykładowe opcje: Technologia, Sport, Muzyka, Podróże, Film.
  • Użytkownik może zaznaczyć dowolną liczbę opcji (od zera do wszystkich).
  • (Opcjonalnie) Możesz wymagać, by co najmniej jedna opcja była zaznaczona, jeśli ma to sens w danym scenariuszu. W naszym przypadku załóżmy, że przynajmniej jedna kategoria musi zostać wybrana (bo np. użytkownik ma wybrać zainteresowania do subskrypcji treści).
  • Po wysłaniu formularza aplikacja powinna wyświetlić podsumowanie: listę wybranych przez użytkownika kategorii. Jeśli nie zaznaczono żadnej (i to dozwolone), wyświetl komunikat, że nic nie wybrano lub komunikat błędu (jeśli wymagane było zaznaczenie).

Kroki rozwiązania:

  1. Przygotowanie formularza HTML: Utwórz formularz z kilkoma polami typu checkbox. Ważne: wszystkie checkboxy dotyczące tej listy muszą mieć taką samą nazwę zakończoną [], co sygnalizuje PHP, że ma utworzyć tablicę wartości. Np. <input type=”checkbox” name=”interests[]” value=”Technologia”> Technologia i analogicznie dla innych opcji​
  1. Odbiór danych w PHP: Po wysłaniu formularza (method POST) sprawdź w PHP, czy tablica $_POST[’interests’] istnieje. Jeżeli użytkownik nie zaznaczył żadnego checkboxa, to PHP nie utworzy tego klucza w $_POST w ogóle. Można to sprawdzić używając empty($_POST[’interests’]) lub !isset($_POST[’interests’]).
  2. Walidacja wyboru (jeśli wymagane): Jeśli zakładamy, że co najmniej jedna opcja musi być wybrana, a tablica nie istnieje lub jest pusta, dodaj komunikat błędu typu „Wybierz przynajmniej jedną kategorię”. W przeciwnym razie, pobierz tablicę zaznaczonych opcji.
  3. Przetworzenie zaznaczonych opcji: Załóżmy, że w zmiennej $selected mamy tablicę zaznaczonych kategorii (np. [„Technologia”, „Muzyka”]). Możemy teraz np. wyświetlić je w formie listy lub pojedynczego komunikatu. Można iterować po tablicy za pomocą foreach i wypisywać kolejne elementy, albo połączyć je w jeden string (np. implode(„, „, $selected)).
  4. Wyświetlenie podsumowania: Jeśli wszystko jest OK, pokaż użytkownikowi komunikat zawierający wybrane przez niego opcje. Jeśli wystąpił błąd (np. nic nie zaznaczono, a było wymagane), wyświetl ten błąd i ewentualnie ponownie formularz do wyboru.

Dane wejściowe:

 $selected = [];  // tablica na wybrane opcje

 $error = "";

Zadanie 4: Formularz przesyłania pliku (upload pliku)

Opis sytuacji:
W aplikacji internetowej chcemy umożliwić użytkownikowi przesłanie pliku na serwer – na przykład zdjęcia profilowego lub dokumentu PDF. Twoim zadaniem jest przygotowanie formularza oraz obsługi w PHP, która pozwoli na wgranie pliku, z weryfikacją podstawowych kryteriów (typ i rozmiar pliku) oraz z zapisaniem go w określonym miejscu na serwerze. Uprościmy założenie: nie zapisujemy informacji o pliku w bazie danych, chodzi tylko o samo przesyłanie i zapis w katalogu.

Wymagania funkcjonalne:

  • Formularz HTML z polem Plik do wysłania (typ file) oraz przyciskiem submit. Formularz musi mieć ustawiony atrybut enctype=”multipart/form-data” oraz method POST, aby przesyłanie pliku zadziałało.
  • Po stronie PHP należy obsłużyć superglobalną tablicę $_FILES. W szczególności:
    • Sprawdzić, czy plik w ogóle został wybrany i przesłany (np. $_FILES[’nazwa’][’error’] czy nie wskazuje błędu UPLOAD_ERR_NO_FILE).
    • Zawęzić dozwolone typy plików – np. przyjmujemy tylko obrazki (JPEG, PNG, GIF) albo tylko pliki PDF, w zależności od scenariusza. Można sprawdzić rozszerzenie pliku lub typ MIME.
    • Ograniczyć maksymalny rozmiar pliku (np. do 2 MB). Warto pamiętać, że w ustawieniach PHP istnieją limity upload_max_filesize i post_max_size, ale dodatkowo możemy własnoręcznie zweryfikować $_FILES[’nazwa’][’size’].
    • W razie niespełnienia wymagań (brak pliku, zły format, za duży rozmiar) – wyświetlić komunikat błędu i nie zapisywać pliku.
    • Jeśli wszystko jest OK – zapisać plik na serwerze (np. do katalogu uploads/). Użyć do tego funkcji move_uploaded_file($_FILES[’nazwa’][’tmp_name’], $sciezka_docelowa). Upewnić się, że katalog docelowy istnieje i ma prawa zapisu dla skryptu.
    • Dla bezpieczeństwa dobrze jest zmienić nazwę pliku (np. dodać unikalny prefix lub ID) aby uniknąć nadpisania istniejących plików o tej samej nazwie oraz potencjalnych problemów z nietypowymi nazwami.
  • Po zapisaniu pliku wyświetl użytkownikowi komunikat potwierdzający, np. „Plik XYZ został przesłany pomyślnie.” Ewentualnie można wypisać pewne informacje o pliku (nazwę, rozmiar) lub zaoferować podgląd (np. wyświetlić obrazek, jeśli to zdjęcie).

Kroki rozwiązania:

  1. Formularz HTML do uploadu: Utwórz formularz z enctype=”multipart/form-data” i metodą POST. Umieść w nim pole <input type=”file” name=”userfile”> (np. o nazwie „userfile” lub innej) oraz przycisk submit. Możesz dodać również pola tekstowe, np. opis pliku, ale nie jest to konieczne do demonstracji mechanizmu.
  2. Sprawdzenie przesłania pliku: W skrypcie PHP użyj $_FILES. Najpierw sprawdź $_FILES[’userfile’][’error’]. Jeśli równe UPLOAD_ERR_NO_FILE (wartość 4) lub tablica $_FILES w ogóle nie istnieje, to znaczy, że użytkownik nie wybrał pliku – zwróć błąd „Nie wybrano pliku.”.
  3. Walidacja typu i rozmiaru: Jeśli plik jest przesłany, sprawdź jego typ/rozszerzenie. Możesz np. pobrać nazwę pliku $_FILES[’userfile’][’name’] i za pomocą pathinfo($nazwa, PATHINFO_EXTENSION) wyciągnąć rozszerzenie, a następnie sprawdzić, czy znajduje się ono na liście dozwolonych (np. [’jpg’,’png’,’gif’,’pdf’]). Innym podejściem jest sprawdzenie typu MIME $_FILES[’userfile’][’type’] lub użycie funkcji mime_content_type na pliku tymczasowym. Sprawdź też rozmiar w bajtach $_FILES[’userfile’][’size’] – np. czy nie przekracza 2 000 000 bajtów (~2MB). W razie wykrycia nieprawidłowego typu lub zbyt dużego pliku, przygotuj komunikat błędu (np. „Niedozwolony typ pliku” lub „Plik jest zbyt duży”).
  4. Przygotowanie nazwy i ścieżki docelowej: Ustal nazwę, pod jaką plik zostanie zapisany na serwerze. Można użyć oryginalnej nazwy (ale lepiej dodać do niej prefiks/sufiks). Np. $filename = time() . „_” . basename($_FILES[’userfile’][’name’]); – to dołącza znacznik czasu, co zwiększa unikalność. Katalog docelowy ustaw np. $uploadDir = „uploads/”; (upewnij się, że istnieje). Pełna ścieżka: $targetPath = $uploadDir . $filename;.
  5. Zapis pliku na serwerze: Użyj move_uploaded_file($_FILES[’userfile’][’tmp_name’], $targetPath). Ta funkcja przeniesie plik z katalogu tymczasowego (gdzie trafia podczas uploadu) do wskazanej lokalizacji. Zwraca true/false w zależności od powodzenia operacji.
  6. Wyświetlenie wyniku: Jeśli zapis się powiódł, wyświetl komunikat sukcesu. Możesz np. podać nazwę pliku lub nawet wyświetlić link do niego (jeśli to bezpieczne) albo osadzić obraz (jeśli to obrazek, <img src=’uploads/nazwa.jpg’>). Jeśli wystąpił błąd (np. funkcja zwróci false lub nie przeszedł walidacji), wyświetl komunikat błędu i (opcjonalnie) ponownie formularz.

Dane wejściowe:

$uploadDir = __DIR__ . "/uploads/";       // katalog docelowy na serwerze (ścieżka fizyczna)

$allowedExt = ['jpg','jpeg','png','gif']; // dozwolone rozszerzenia plików obrazów

$maxSize = 2 * 1024 * 1024;              // 2 MB w bajtach

$message = "";

Zadanie 5: Prosty quiz z oceną wyniku

Opis sytuacji:
Przygotuj prosty quiz jednokrotnego wyboru składający się z kilku pytań. Użytkownik udziela odpowiedzi poprzez zaznaczenie jednej z opcji przy każdym pytaniu (radio button). Po wysłaniu formularza skrypt powinien sprawdzić odpowiedzi z wcześniej zdefiniowanymi prawidłowymi odpowiedziami i wyświetlić podsumowanie – np. ile odpowiedzi było poprawnych, ewentualnie które. Taki mechanizm łączy w sobie obsługę wielu pól formularza oraz prostą logikę porównywania i zliczania wyniku.

Wymagania funkcjonalne:

  • Formularz zawiera 3 pytania (dla przykładu) z dziedziny ogólnej wiedzy. Każde pytanie ma jedną prawidłową odpowiedź spośród kilku dostępnych (np. 3 opcji oznaczonych A, B, C).
  • Każde pytanie jest przedstawione jako tekst pytania oraz poniżej zestaw opcji do wyboru (radio). Użytkownik musi wybrać dokładnie jedną odpowiedź do każdego pytania.
  • Po wysłaniu formularza następuje walidacja, czy na wszystkie pytania udzielono odpowiedzi. Jeśli brakuje odpowiedzi na któreś pytanie, zwracany jest komunikat błędu proszący o udzielenie brakujących odpowiedzi (i ponowne wyświetlenie formularza).
  • Jeśli na wszystkie pytania są odpowiedzi, skrypt porównuje je z kluczem poprawnych odpowiedzi i oblicza liczbę poprawnych odpowiedzi.
  • Wynik quizu jest prezentowany użytkownikowi, np. w formie: „Twój wynik: 2/3 poprawnych odpowiedzi.” Można też opcjonalnie wskazać prawidłowe odpowiedzi dla pytań, ale podstawowo chodzi o samą punktację.

Kroki rozwiązania:

  1. Przygotowanie pytań i formularza HTML: Określ treść 3 pytań i po 3 opcje odpowiedzi do każdego (A, B, C). Dla każdego pytania użyj pola typu radio z taką samą nazwą (np. name=”q1″, name=”q2″, name=”q3″), aby grupy były rozłączne. Każda opcja powinna mieć unikalną wartość (np. „A”, „B”, „C”) oraz etykietę z treścią odpowiedzi. Możesz dopisać literę przy każdej opcji lub zawrzeć ją w etykiecie.
  2. Definicja prawidłowych odpowiedzi: W kodzie PHP zdefiniuj strukturę (np. tablicę asocjacyjną), która będzie przechowywała poprawne odpowiedzi dla poszczególnych pytań. Np. $correctAnswers = [’q1’=>’B’, 'q2’=>’B’, 'q3’=>’A’]; oznacza, że dla pytania 1 poprawna jest opcja B, dla pytania 2 opcja B, dla pytania 3 opcja A.
  3. Sprawdzenie kompletności odpowiedzi: Po wysłaniu formularza (metodą POST) sprawdź, czy w $_POST istnieją klucze dla wszystkich pytań. Jeśli któregoś brakuje (co oznacza, że użytkownik nie zaznaczył żadnej odpowiedzi w danej grupie), dodaj komunikat błędu, np. „Nie udzielono odpowiedzi na pytanie 2.” Można te komunikaty przygotować dynamicznie, iterując po tablicy pytań.
  4. Ocena odpowiedzi: Jeżeli wszystkie pytania mają zaznaczone odpowiedzi, porównaj je z kluczem. Iteruj po tablicy z poprawnymi odpowiedziami i sprawdzaj, czy $_POST[’qi’] (gdzie i to numer pytania) jest równe odpowiedzi w kluczu. Zliczaj poprawne trafienia w zmiennej licznikowej (np. $score).
  5. Wyświetlenie wyniku: Jeżeli były braki, wyświetl ponownie formularz z komunikatami o błędach (i zachowaniem zaznaczonych wcześniej odpowiedzi). Jeśli wszystko wypełnione, wyświetl rezultat quizu, np. „Uzyskałeś 2 z 3 punktów.” Ewentualnie możesz podać procent lub krótki komentarz (np. „dobry wynik” itp. według uznania).

Dane wejściowe:

 $correctAnswers = [

     "q1" => "B",

     "q2" => "B",

     "q3" => "A"

 ];

 $userAnswers = [];  // tablica na odpowiedzi użytkownika

 $errors = [];

 // Pytania i opcje (do ułatwienia generowania formularza i komunikatów)

 $questions = [

     "q1" => "Stolica Francji to:",
 
     "q2" => "2 + 2 = ?",

     "q3" => "PHP to język:"

 ];

 $options = [

     "q1" => ["A" => "Londyn", "B" => "Paryż", "C" => "Rzym"],

     "q2" => ["A" => "3", "B" => "4", "C" => "5"],

     "q3" => ["A" => "skryptowy", "B" => "styli (CSS)", "C" => "znaczników (HTML)"]

 ];

Obsługa formularzy HTML w języku PHP

Wprowadzenie do formularzy HTML i PHP

Formularze HTML umożliwiają interakcję użytkownika z aplikacją webową – pozwalają na wprowadzenie danych (np. tekstu, wyboru opcji) i przesłanie ich na serwer. PHP, jako skrypt po stronie serwera, potrafi odbierać dane z formularzy, przetwarzać je i generować odpowiedź (np. wyświetlić wyniki lub komunikaty). Proces zwykle wygląda następująco:

  • Formularz HTML zdefiniowany w kodzie strony (za pomocą elementu <form>) zawiera pola input (np. pole tekstowe, checkbox, itp.) oraz przycisk submit do wysłania danych.
  • Gdy użytkownik wypełni formularz i wyśle go (klikając przycisk Wyślij), przeglądarka tworzy żądanie HTTP do serwera. Dane z formularza są kodowane i dołączane do tego żądania (w URL lub w treści żądania – zależnie od metody).
  • Na serwerze skrypt PHP (określony przez atrybut action formularza) odbiera to żądanie. PHP udostępnia superglobalne zmienne (takie jak $_GET i $_POST), przez które możemy odczytać przesłane wartości pól formularza.
  • Skrypt PHP może następnie wykonać różne operacje na otrzymanych danych: np. walidację (sprawdzenie poprawności), obliczenia, zapis do pliku/bazy (tego nie omawiamy tutaj) lub przygotowanie odpowiedzi HTML.
  • Ostatecznie PHP zwraca wynik (HTML wygenerowany dynamicznie) do przeglądarki, która wyświetla go użytkownikowi. Może to być np. strona potwierdzająca przyjęcie danych lub wyświetlająca przesłane informacje.

W dalszych sekcjach omówimy krok po kroku kluczowe zagadnienia związane z obsługą formularzy w PHP: konfigurację formularza HTML, różnice między metodami przesyłania danych (GET i POST), odbieranie danych po stronie PHP, podstawową walidację tych danych oraz obsługę błędów i wyświetlanie komunikatów dla użytkownika. Na końcu zaprezentujemy praktyczne przykłady kodu oraz propozycje ćwiczeń do samodzielnej realizacji.

(Uwaga: Ten wykład skupia się na podstawach obsługi formularzy w PHP dla początkujących i nie obejmuje zagadnień baz danych, takich jak MySQL.)

Metody GET i POST – różnice i zastosowanie

HTML udostępnia dwa główne sposoby wysyłania danych formularza w żądaniu HTTP: metodę GET i POST. Wybór metody odbywa się poprzez atrybut method w tagu <form>. Choć obie metody mogą przesłać te same informacje, różnią się sposobem transmisji i zastosowaniami:

  • Metoda GET:
    • Dane formularza są dołączane do adresu URL jako ciąg zapytania (query string). Przykład: po wysłaniu formularza na index.php z polem name o wartości „Jan”, adres może wyglądać tak:
      index.php?name=Jan
      Wszystkie pary nazwa_pola=wartość stają się częścią URL (po znaku ?, rozdzielone & jeśli jest ich wiele).
    • Zawartość wysyłanych pól jest widoczna w pasku adresu przeglądarki. Umożliwia to m.in. łatwe testowanie i bookmarkowanie (dodanie do zakładek) wyników – np. wyniki wyszukiwania można zapisać jako URL z zapytaniem.
    • Ograniczenia: Nie należy używać GET do przesyłania wrażliwych danych (hasła, dane osobowe), ponieważ adres URL może być zapisywany w historii przeglądarki i logach serwera. Ponadto URL ma ograniczoną długość – w praktyce można przesłać około 1024 do 2048 znaków (zależnie od przeglądarki), co ogranicza wielkość danych.
    • Zastosowania: GET jest zalecany, gdy formularz pobiera dane lub wykonuje operację bez skutków ubocznych na serwerze. Np. formularze wyszukiwarek używają GET – zapytanie nie zmienia danych na serwerze, a wyniki można łatwo udostępnić poprzez URL. Również do nawigacji (filtrowanie, sortowanie) często stosuje się GET.
  • Metoda POST:
    • Dane formularza są wysyłane w ciele (body) żądania HTTP, a nie przez URL. Dzięki temu parametry nie są bezpośrednio widoczne w adresie strony.
    • Brak ograniczeń długości w praktycznym ujęciu – POST pozwala przesłać większe ilości danych (np. długie wpisy tekstowe). Metoda ta jest także używana do przesyłania plików (wymaga dodatkowo ustawienia enctype=”multipart/form-data” w formularzu, co jednak jest poza zakresem tego wykładu).
    • Dane przesłane POSTem nie pojawiają się w historii ani zakładkach – jeśli odświeżysz stronę po wysłaniu POST, przeglądarka ostrzeże przed ponownym wysłaniem formularza (co mogłoby np. spowodować dodanie tego samego wpisu drugi raz).
    • Zastosowania: POST jest zalecany, gdy formularz modyfikuje stan serwera lub przesyła wrażliwe dane. Na przykład formularze logowania (hasła), rejestracji, dodawania komentarzy używają metody POST. Ogólnie, operacje typu „utwórz/zapisz/wyślij” wykorzystują POST, aby dane nie były ujawniane w URL i aby uniknąć ograniczeń długości.

Podsumowanie: Wybór metody jest ważny. GET nadaje się do zapytań, filtrów i wszelkich działań „czytających”, natomiast POST do działań „zmieniających” lub przesyłania danych, których nie chcemy ujawniać. W kodzie PHP odpowiednie superglobalne tablice ($_GET lub $_POST) pozwalają odczytać dane – należy ich używać zależnie od wybranej metody (nie można odebrać danych POST przez $_GET i vice versa).

Przykład definicji formularza z określeniem metody:

 <form action="przetwarzaj.php" method="GET">

     <!-- pola formularza tutaj -->

     <input type="text" name="imie">

     <input type="submit" value="Wyślij">

 </form>

W powyższym kodzie dane będą wysłane metodą GET do skryptu przetwarzaj.php. Zmieniając method=”GET” na method=”POST”, prześlemy dane metodą POST. W dalszej części zobaczymy, jak te dane odebrać po stronie PHP.

Odbieranie danych z formularza w PHP

Po wysłaniu formularza, skrypt PHP wskazany w atrybucie action formularza zostaje wywołany. W PHP do dostępu do przesłanych danych używamy superglobalnych tablic asocjacyjnych: głównie $_GET i $_POST. Są to zmienne typu tablica dostępne w każdym miejscu skryptu, zawierające pary klucz-wartość odpowiadające nazwom pól formularza i ich zawartości.

Jak to działa? Jeśli w formularzu mamy pole <input type=”text” name=”imie”> i użytkownik wpisał „Jan”, to po wysłaniu:

  • Jeżeli formularz użył metody GET, PHP umieści tę wartość pod kluczem „imie” w tablicy $_GET. Czyli $_GET[„imie”] zwróci string „Jan”.
  • Jeżeli formularz użył metody POST, analogicznie wartość będzie dostępna jako $_POST[„imie”].

Przykładowo, wyobraźmy sobie formularz HTML wysyłający imię i adres e-mail metodą POST do skryptu wynik.php. Kod HTML formularza (plik formularz.html lub .php) mógłby wyglądać tak:

 <form action="wynik.php" method="POST">

    Imię: <input type="text" name="imie"><br>

    E-mail: <input type="email" name="email"><br>

    <input type="submit" value="Wyślij">

 </form>

Po wciśnięciu „Wyślij” przeglądarka wywoła wynik.php i przekaże dane. W pliku wynik.php możemy odebrać i wyświetlić te dane następująco:

<?php

// wynik.php - skrypt odbierający dane z formularza

$imie = $_POST["imie"];    // pobranie wartości pola 'imie'

$email = $_POST["email"];  // pobranie wartości pola 'email'

// Wyświetlenie odebranych danych

echo "<h3>Witaj, $imie!</h3>";

echo "<p>Twój podany email to: $email</p>";

?>

Jeśli formularz zostałby zmieniony na method=”GET”, w powyższym skrypcie wystarczyłoby użyć $_GET zamiast $_POST. Poza tym odbiór danych przebiega tak samo.

Ważne uwagi:

  • Zanim użyjemy wartości z $_POST czy $_GET, dobrze jest sprawdzić, czy dane pole zostało przesłane. Można to zrobić za pomocą funkcji isset(). Np. isset($_POST[„imie”]) sprawdzi, czy klucz „imie” istnieje (formularz został wysłany i to pole ma jakąś wartość, nawet pusty ciąg).
  • PHP oferuje też tablicę $_REQUEST, która zawiera dane zarówno z GET, jak i POST (oraz ciasteczek). Jednak dla przejrzystości i bezpieczeństwa lepiej korzystać z konkretnych $_GET lub $_POST w zależności od potrzeb.
  • Wszystkie wartości z formularza trafiają jako łańcuchy tekstowe (string). Nawet jeśli pole input jest typu number, w PHP otrzymamy wartość jako string i np. aby wykonać operacje arytmetyczne, trzeba ją przekonwertować (np. (int) $_POST[„wiek”] dla liczby całkowitej).
  • Nazwy pól formularza (atrybuty name) rozróżniają wielkość liter po stronie PHP (to klucze tablicy). Np. $_POST[„Imie”] to co innego niż $_POST[„imie”]. W HTML atrybut name również powinien być unikalny w ramach formularza dla każdego pola, aby dane były poprawnie odebrane.

Podstawowa walidacja danych formularza

Walidacja danych to proces sprawdzania, czy dane wprowadzone przez użytkownika spełniają oczekiwane kryteria (np. czy wymagane pola nie są puste, czy email ma poprawny format). Walidacja jest kluczowa, ponieważ chroni naszą aplikację przed nieprawidłowymi danymi lub nawet złośliwymi wpisami.

Podstawowe kroki walidacji, które omówimy:

  1. Sprawdzenie wymaganych pól – czy użytkownik wypełnił wszystkie pola, które muszą mieć wartość.
  2. Sprawdzenie formatu danych – np. czy w polu e-mail rzeczywiście jest adres e-mail, a nie ciąg znaków bez @, itp.
  3. Inne proste reguły – np. czy liczba mieści się w oczekiwanym zakresie, czy tekst nie przekracza określonej długości, czy w polu imię są tylko litery, itd. (Dla początkujących skupimy się na kilku podstawowych przykładach).

Walidację można wykonywać po stronie klienta (w przeglądarce, np. za pomocą HTML5 lub JavaScript) i po stronie serwera (w PHP). Należy pamiętać, że walidacja po stronie klienta jest dodatkowa – zawsze musimy sprawdzić dane w PHP, ponieważ użytkownik może ominąć lub wyłączyć skrypty w przeglądarce. Poniżej skupiamy się na walidacji w PHP (serwerowej).

Załóżmy, że mamy formularz z polami: imie, email oraz wiadomość. Chcemy sprawdzić, czy imie i email nie są puste, a także czy email ma poprawny format. Przykładowe podejście:

  • Sprawdzanie, czy pole jest puste: Możemy użyć funkcji empty() lub sprawdzić, czy po trim (usunięciu białych znaków) długość stringa > 0.
    Przykład:
 if (empty($_POST["imie"])) {

    // imie jest puste

 }

lub bardziej precyzyjnie:

 $imie = trim($_POST["imie"]);

 if ($imie == "") {

    // imie jest puste lub zawierało tylko spacje

 }
  • Walidacja adresu e-mail: Najprostszym sposobem jest użycie wbudowanego filtra FILTER_VALIDATE_EMAIL. Można go użyć z funkcją filter_var().
    Przykład:
 $email = $_POST["email"];

 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

    // email jest niepoprawny

 }

Powyższa funkcja zwróci false, jeśli $email nie zawiera poprawnie sformatowanego adresu (czyli musi mieć przynajmniej znak @ i kropkę po nim, bez niedozwolonych znaków).

  • Inne walidacje: Możemy sprawdzić długość tekstu (np. strlen($pole) > 255 aby ograniczyć długość wpisu), czy w imieniu są tylko litery (np. używając wyrażenia regularnego preg_match(’/^[A-Za-z\s]+$/’, $imie)), czy liczba jest rzeczywiście liczbą (is_numeric($wiek)). Na poziomie podstawowym często wystarczy sprawdzenie niepustych pól i kluczowych formatów (email, ewentualnie liczby).

Walidację przeprowadzamy po odebraniu danych z formularza (czyli np. po sprawdzeniu $_SERVER[„REQUEST_METHOD”] == „POST” lub po kliknięciu submit). Najczęściej tworzy się zestaw zmiennych na błędy (np. $error_imie, $error_email) i jeśli kryterium nie jest spełnione, przypisuje się tym zmiennym komunikat o błędzie. Gdy wszystkie dane przejdą walidację, można bezpiecznie je wykorzystać (np. wyświetlić lub przetworzyć dalej).

Przykład fragmentu PHP walidującego dwa pola z formularza:

 $errors = [];  // tablica na komunikaty błędów

 // Sprawdzenie pola "imie"

 $imie = trim($_POST["imie"] ?? "");

 if ($imie === "") {

     $errors["imie"] = "Pole imię nie może być puste.";

 }

 // Sprawdzenie pola "email"

 $email = trim($_POST["email"] ?? "");

 if ($email === "") {

     $errors["email"] = "Pole e-mail jest wymagane.";

 } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

     $errors["email"] = "Nieprawidłowy format adresu e-mail.";

 }

//... (ewentualnie kolejne pola)

W powyższym kodzie użyto operatora ?? aby w razie braku klucza w tablicy $_POST użyć domyślnej pustej wartości (zapobiega to błędom Notice). Zmienna $errors gromadzi komunikaty – kluczami są nazwy pól, co ułatwia późniejsze odniesienie się do konkretnych błędów.

Dlaczego walidacja jest ważna?
Poza poprawnością danych dla logiki aplikacji, walidacja chroni również przed nadużyciami. Np. wpisanie tagów HTML lub skryptów <script> w polu formularza mogłoby bez walidacji (lub filtrowania) skutkować wstrzyknięciem niepożądanego kodu w wyświetlanej stronie (tzw. atak XSS – Cross-Site Scripting). Dlatego często podczas walidacji lub przed wyświetleniem danych używa się funkcji PHP takich jak htmlspecialchars() do zamiany < > i innych specjalnych znaków na encje HTML (żeby ewentualny wpis <script> został wyświetlony jako ciąg znaków, a nie wykonany). Na przykład:

echo htmlspecialchars($userInput);

sprawi, że nawet jeśli $userInput zawiera <b>tekst</b> lub <script>…, to na stronie zobaczymy bezpiecznie &lt;b&gt;tekst&lt;/b&gt; (czyli dosłowny napis, bez pogrubienia czy wykonania skryptu).

Podsumowując, zawsze ufaj, ale sprawdzaj dane od użytkownika. W następnym rozdziale omówimy, jak informować użytkownika o ewentualnych błędach walidacji.

Obsługa błędów i komunikaty dla użytkownika

Gdy skrypt PHP wykryje błędne lub brakujące dane w formularzu, powinien poinformować o tym użytkownika w czytelny sposób, aby ten mógł poprawić swoje wpisy. Obsługa błędów formularza zazwyczaj obejmuje:

  • Zatrzymanie dalszego przetwarzania niepoprawnych danych – np. nie wysyłamy formularza do bazy ani nie wykonujemy akcji, jeśli wykryto błędy.
  • Przechowanie komunikatów o błędach – zwykle w zmiennych lub tablicy (jak $errors powyżej), tak aby można je było wyświetlić wraz z formularzem.
  • Ponowne wyświetlenie formularza z informacją o błędach – użytkownik powinien zobaczyć, które pola wymagają poprawy, i móc je poprawić bez przepisywania wszystkiego od zera. Dlatego często stosuje się mechanizm „sticky form”, czyli wypełnianie formularza ponownie poprzednio wpisanymi wartościami (żeby np. imię pozostało w polu, jeśli błąd dotyczył innego pola).

Typowy przepływ obsługi błędów w jednym skrypcie PHP:

  1. Sprawdzenie, czy formularz został wysłany. Często używamy:
if ($_SERVER["REQUEST_METHOD"] == "POST") {

    // ... przetwarzaj formularz

}

lub sprawdzamy isset($_POST[’nazwa_pola’]) czy np. isset($_POST[’submit’]) (jeśli przycisk submit ma name).

  1. Walidacja danych (jak opisano wyżej). Wykrywamy błędy i zapisujemy komunikaty.
  2. Jeśli są błędy: wyświetlamy ponownie formularz z komunikatami i zachowanymi poprzednimi wartościami pól.
    Jeśli brak błędów: możemy bezpiecznie skorzystać z danych – np. wyświetlić podsumowanie („Dziękujemy za wysłanie formularza, oto Twoje dane…”) lub wykonać inną akcję (wysłać email z informacją, zapisać do pliku itp.).
  3. Wyświetlanie komunikatów błędów: Można to zrobić na różne sposoby:
    • W formie listy nad formularzem (wypisując wszystkie komunikaty).
    • Przy każdym polu osobno – np. obok pola „email” czerwoną czcionką tekst „Wprowadź poprawny email”. To wymaga, aby w kodzie HTML formularza umieścić np. <?php echo $errors[’email’] ?? ” ?> w odpowiednim miejscu.
    • Wyskakujące alerty JS lub inne mechanizmy front-end (to jednak wykracza poza czysty PHP i wymaga JS; na poziomie podstaw wystarczą komunikaty tekstowe na stronie).

Poniżej znajduje się fragment kodu PHP ilustrujący obsługę błędów i ponowne wyświetlanie formularza. Łączymy tu razem walidację i generowanie HTML z formularzem w jednym pliku PHP:

 <?php

 // Inicjalizacja zmiennych dla wartości pól i błędów

 $imie = $email = $wiadomosc = "";

 $errors = ["imie" => "", "email" => "", "wiadomosc" => ""];

 // Sprawdź, czy formularz został wysłany metodą POST

 if ($_SERVER["REQUEST_METHOD"] == "POST") {

     // Walidacja pola "imie"
 
    $imie = trim($_POST["imie"] ?? "");

    if ($imie === "") {

        $errors["imie"] = "Pole imię jest wymagane.";

    }

    // Walidacja pola "email"

    $email = trim($_POST["email"] ?? "");

    if ($email === "") {

        $errors["email"] = "Pole e-mail jest wymagane.";

    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

        $errors["email"] = "Nieprawidłowy format adresu e-mail.";

    }

    // Walidacja pola "wiadomosc"

    $wiadomosc = trim($_POST["wiadomosc"] ?? "");

    if ($wiadomosc === "") {

        $errors["wiadomosc"] = "Pole wiadomość nie może być puste.";

    }

    // Sprawdź, czy nie ma żadnych błędów

    $czy_blad = false;

    foreach ($errors as $err) {

        if ($err != "") {

            $czy_blad = true;

            break;

        }

    }

    if (!$czy_blad) {

        // Brak błędów – tutaj moglibyśmy np. wysłać dane do bazy lub maila.

        // W tym przykładzie po prostu wyświetlimy podsumowanie.

        echo "<h3>Dziękujemy za przesłanie formularza!</h3>";

        echo "<p><strong>Imię:</strong> " . htmlspecialchars($imie) . "</p>";

        echo "<p><strong>E-mail:</strong> " . htmlspecialchars($email) . "</p>";

        echo "<p><strong>Wiadomość:</strong> " . nl2br(htmlspecialchars($wiadomosc)) . "</p>";

        // Zakończ dalsze wykonywanie skryptu, aby nie wyświetlać ponownie formularza

        exit;

    }

 }

 ?>

 <!-- Formularz HTML wyświetlany zarówno na początku, jak i w razie błędów -->

 <form method="POST" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">

    <label>Imię:

       <input type="text" name="imie" value="<?php echo htmlspecialchars($imie); ?>">

       <span style="color:red;"><?php echo $errors["imie"]; ?></span>

    </label><br><br>

    <label>E-mail:

       <input type="text" name="email" value="<?php echo htmlspecialchars($email); ?>">

       <span style="color:red;"><?php echo $errors["email"]; ?></span>

    </label><br><br>

    <label>Wiadomość: <br>

       <textarea name="wiadomosc" rows="5" cols="40">
             <?php echo htmlspecialchars($wiadomosc); ?>
       </textarea>

       <span style="color:red;"><?php echo $errors["wiadomosc"]; ?></span>

    </label><br><br>

    <input type="submit" value="Wyślij">

 </form>

W powyższym kodzie warto zwrócić uwagę na kilka rzeczy:

  • Użyliśmy $_SERVER[„PHP_SELF”] w atrybucie action formularza, opakowane w htmlspecialchars(). To sprawia, że formularz wysyła dane do samego siebie (do bieżącego skryptu PHP). Dzięki temu możemy w jednym pliku obsłużyć zarówno wyświetlanie formularza, jak i przetwarzanie danych po wysłaniu. htmlspecialchars() zabezpiecza ewentualne specjalne znaki (to zabezpieczenie przed nietypowymi sytuacjami, gdyby nazwa skryptu zawierała np. znak &).
  • Po przetworzeniu formularza i wykryciu braku błędów, wykorzystaliśmy exit; aby zakończyć wykonywanie skryptu po wyświetleniu podziękowania i podsumowania. Gdyby tego nie zrobić, skrypt kontynuowałby i wyświetlił ponownie formularz pod podziękowaniem (co w tym przypadku nie jest pożądane).
  • Funkcja nl2br() została użyta przy wyświetleniu wiadomości – zamienia ona znaki nowej linii na <br>, aby np. tekst wielolinijkowy wpisany w <textarea> wyświetlił się z zachowaniem nowych linii.
  • Wszystkie wyświetlane dane pochodzące od użytkownika przepuszczamy przez htmlspecialchars(). Zapobiega to potencjalnemu wstrzyknięciu kodu HTML/JS. Jest to dobra praktyka przy wyświetlaniu danych od użytkownika (walidacja to osobny etap – tutaj dbamy o bezpieczne prezentowanie).
  • Błędy przypisaliśmy do <span style=”color:red;”> obok odpowiednich pól, aby użytkownik widział konkretnie, co należy poprawić.

Powyższy przykład pokazuje, jak może wyglądać kompleksowa obsługa formularza z walidacją i komunikatami o błędach w jednym pliku PHP. Alternatywnie, można podzielić to na dwa pliki (jeden z formularzem HTML, drugi z logiką PHP i wyświetlaniem wyników), ale wtedy trudniej jest zrobić „powrót” do formularza z zachowaniem już wpisanych danych.

Przykłady praktyczne

W tej sekcji zademonstrujemy dwie pełne implementacje obsługi formularza w PHP:

  1. Prosty formularz i wyświetlenie danych (bez walidacji) – pokazuje mechanizm przesłania i odbioru danych przy użyciu osobnych plików dla formularza i skryptu PHP.
  2. Formularz z walidacją i obsługą błędów (jeden plik) – bardziej rozbudowany przykład ilustrujący wprowadzone wyżej zasady walidacji i komunikatów, w ramach jednego skryptu.

Przykład 1: Prosty formularz (GET/POST) i odbiór danych

Opis: Utworzymy prosty formularz z dwoma polami: imię i ulubiony kolor. Użytkownik wypełnia pola i wysyła formularz. Skrypt PHP odbiera dane i wyświetla komunikat powitalny. Nie będziemy tutaj wykonywać walidacji – zakładamy, że użytkownik wpisał dane.

Plik HTML z formularzem (formularz.html):

 <!DOCTYPE html>

 <html lang="pl">

 <head>

   <meta charset="UTF-8">

   <title>Formularz powitalny</title>

 </head>

 <body>

 <h2>Witaj! Wypełnij formularz:</h2>

 <form action="odbierz.php" method="POST">

     <label>Imię: <input type="text" name="imie"></label><br><br>

     <label>Ulubiony kolor: <input type="text" name="kolor"></label><br><br>

     <input type="submit" value="Wyślij">

 </form>

 </body>

 </html>

Kilka wyjaśnień do powyższego kodu:

  • Atrybut action=”odbierz.php” oznacza, że dane zostaną wysłane do skryptu o nazwie odbierz.php (musimy go napisać za moment). Można tu podać również pełny URL lub ścieżkę do skryptu obsługi.
  • method=”POST” wskazuje, że używamy metody POST (dzięki czemu dane nie pojawią się w adresie URL).
  • Każde pole input ma atrybut name – odpowiednio „imie” i „kolor”. Te nazwy posłużą jako klucze w tablicy $_POST po przesłaniu.
  • Używamy prostych pól tekstowych (type=”text”). Ewentualnie dla koloru można by użyć <input type=”color”> lub listy wyboru, ale dla prostoty pozostajemy przy tekście.

Plik PHP obsługujący formularz (odbierz.php):

 <?php

 // Upewnijmy się, że skrypt został wywołany metodą POST

 if ($_SERVER["REQUEST_METHOD"] == "POST") {

     // Pobranie danych z tablicy $_POST

     $imie = $_POST["imie"] ?? "";

     $kolor = $_POST["kolor"] ?? "";

     // Opcjonalnie: proste "walidacje" - w tym przykładzie tylko sprawdzimy czy nie są puste

     if ($imie == "" || $kolor == "") {

         echo "Nie podano imienia lub koloru. Wróć i uzupełnij formularz.";

         exit; // zakończ, nie pokazuj dalszej części, jeśli brak danych

     }

     // Bezpieczne wyświetlenie wprowadzonych danych

     $imie_clean = htmlspecialchars($imie);

     $kolor_clean = htmlspecialchars($kolor);

     echo "<h2>Witaj, $imie_clean!</h2>";

     echo "<p>Twój ulubiony kolor to: <span style=\"color:$kolor_clean;\">$kolor_clean</span>.</p>";

} else {

    // Jeśli ten skrypt został wywołany inaczej niż POST (np. bezpośrednie wejście), można przekierować lub wyświetlić komunikat

    echo "Formularz nie został poprawnie wysłany.";

 }

 ?>

Co robi powyższy skrypt:

  • Sprawdza, czy metoda żądania to POST (jeśli ktoś spróbuje wejść na odbierz.php bezpośrednio przez URL, komunikat informuje, że formularz nie został wysłany).
  • Odczytuje wartości imie i kolor z $_POST. Użyto operatora ?? aby w razie braku ustawić pusty string (to zabezpiecza przed sytuacją, że klucz nie istnieje).
  • Proste sprawdzenie: jeśli któreś pole jest puste, wypisujemy komunikat o braku danych i przerywamy działanie (exit).
  • Jeśli dane są dostępne, używamy htmlspecialchars do zneutralizowania ewentualnych tagów HTML wprowadzonych przez użytkownika (np. jeśli ktoś wpisałby „<script>…</script>” jako kolor, to bez tego zabezpieczenia mógłby nam popsuć stronę).
  • Wyświetlamy powitanie i kolor. Użyliśmy span z parametrem style=”color:$kolor_clean;” – to spowoduje, że jeżeli użytkownik wpisał np. „czerwony” to spróbujemy ustawić kolor tekstu na „czerwony”. W większości przeglądarek nazwy kolorów po angielsku działają (np. „red”, „blue”), natomiast wpisanie „czerwony” nie zmieni koloru tekstu (bo CSS nie zna tej nazwy). To tylko przykład – realnie, żeby to działało dla dowolnego koloru, potrzebna by była inna metoda (np. predefiniowana lista lub input typu color). Tu celem jest głównie pokazanie wstawienia wartości w HTML.

Jak to przetestować:
Plik formularz.html otwieramy w przeglądarce (przez serwer, jeśli to .php można też osadzić formularz w .php). Uzupełniamy imię i kolor, wysyłamy. Skrypt odbierz.php powinien wyświetlić powitanie. Np. dla imienia „Jan” i koloru „niebieski” zobaczymy:

Witaj, Jan!

Twój ulubiony kolor to: niebieski.

(Kolor tekstu „niebieski” nie zmieni się, bo CSS nie interpretuje polskiej nazwy koloru – ale gdyby wpisać „blue”, tekst „blue” pojawi się na niebiesko.)

Możesz też zmienić metodę na GET w formularzu i przetestować ponownie. Wtedy zauważysz, że po wysłaniu adres w przeglądarce zmienia się na np. …/odbierz.php?imie=Jan&kolor=niebieski. Skrypt zadziała tak samo (o ile w PHP zmienimy $_POST na $_GET przy odbiorze). To ćwiczenie pomoże zrozumieć różnicę – przy metodzie GET wynik możesz zobaczyć w URL i np. odświeżać stronę bez ponownego ostrzeżenia, natomiast przy POST dane nie pojawiają się w adresie.

Przykład 2: Formularz z walidacją danych i obsługą błędów

Opis: Ten przykład ilustruje kompletne rozwiązanie w jednym pliku PHP: formularz zbiera dane kontaktowe od użytkownika, a skrypt jednocześnie je waliduje i wyświetla odpowiednie komunikaty. Pola formularza: imię (wymagane), e-mail (wymagane, format email), wiadomość (wymagana). Jeśli wszystkie dane są poprawne, wyświetlana jest strona potwierdzająca ich otrzymanie; jeśli nie, formularz pokazuje błędy przy odpowiednich polach.

Skrypt PHP (formularz_kontaktowy.php):

 <?php

 // Inicjalizacja zmiennych dla utrzymania wartości pól i komunikatów błędów

 $imie = $email = $wiadomosc = "";

 $err_imie = $err_email = $err_wiadomosc = "";

 // Sprawdzenie, czy formularz został przesłany

 if ($_SERVER["REQUEST_METHOD"] == "POST") {

    // Pobranie i obróbka danych z formularza

    $imie = trim($_POST["imie"] ?? "");

    $email = trim($_POST["email"] ?? "");

    $wiadomosc = trim($_POST["wiadomosc"] ?? "");

    // Walidacja pola "imie"

    if ($imie === "") {

        $err_imie = "Proszę podać imię.";

    }

    // Walidacja pola "email"

    if ($email === "") {

        $err_email = "Proszę podać adres e-mail.";

    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

        $err_email = "Adres e-mail jest niepoprawny.";

    }

    // Walidacja pola "wiadomosc"

    if ($wiadomosc === "") {

        $err_wiadomosc = "Proszę wpisać treść wiadomości.";

    }

    // Sprawdzenie czy nie ma błędów

    if ($err_imie === "" && $err_email === "" && $err_wiadomosc === "") {

        // Wszystkie pola poprawnie wypełnione

        echo "<h2>Dziękujemy, $imie!</h2>";

        echo "<p>Twoja wiadomość została wysłana poprawnie. Poniżej przesłane dane:</p>";

        echo "<ul>";

        echo "<li><strong>Imię:</strong> " . htmlspecialchars($imie) . "</li>";

        echo "<li><strong>E-mail:</strong> " . htmlspecialchars($email) . "</li>";

        echo "<li><strong>Wiadomość:</strong> " . nl2br(htmlspecialchars($wiadomosc)) . "</li>";

        echo "</ul>";

        // Zakończenie skryptu, aby nie wyświetlać formularza ponownie

        exit;

    }

}

 ?>

 <!DOCTYPE html>

 <html lang="pl">

 <head>

   <meta charset="UTF-8">

   <title>Formularz kontaktowy</title>

   <style>

     .error { color: red; }

   </style>

 </head>

 <body>

 <h2>Formularz kontaktowy</h2>

 <p>Wypełnij poniższe pola i wyślij wiadomość.</p>

 <form action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>" method="POST">

    <p>

      <label>Imię:<br>

      <input type="text" name="imie" value="<?php echo htmlspecialchars($imie); ?>">

      <span class="error"><?php echo $err_imie; ?></span>

      </label>

    </p>

    <p>

      <label>E-mail:<br>

      <input type="text" name="email" value="<?php echo htmlspecialchars($email); ?>">

      <span class="error"><?php echo $err_email; ?></span>

      </label>

    </p>

    <p>

      <label>Wiadomość:<br>

      <textarea name="wiadomosc" rows="5" cols="40"><?php echo htmlspecialchars($wiadomosc); ?></textarea>

      <span class="error"><?php echo $err_wiadomosc; ?></span>

      </label>

    </p>

    <p>

      <input type="submit" value="Wyślij">

    </p>

 </form>

 </body>

 </html> 

Co robi ten skrypt:

  • Przed zdefiniowaniem HTML inicjalizujemy zmienne dla przechowania wpisanych wartości ($imie, $email, $wiadomosc) oraz zmienne na błędy ($err_imie, $err_email, $err_wiadomosc). Dzięki temu, nawet jeśli formularz nie został jeszcze wysłany, te zmienne istnieją (puste) i możemy je użyć w HTML (np. value w inputach).
  • Po wykryciu $_SERVER[„REQUEST_METHOD”] == „POST” następuje pobranie danych z formularza (trim() usuwa zbędne spacje na początku/końcu).
  • Walidujemy każde pole, ustawiając komunikat błędu, jeśli nie spełnia warunków:
    • Imię: musi być podane.
    • Email: musi być podany i poprawny format (filter_var).
    • Wiadomość: musi być podana (nie pusta).
  • Jeśli po walidacji wszystkie zmienne błędów ($err_…) są wciąż puste, to znaczy brak błędów. Wtedy wyświetlamy podziękowanie i listę przesłanych danych, po czym wywołujemy exit aby nie pokazywać formularza.
  • Jeśli którykolwiek błąd wystąpił, skrypt nie kończy się, więc przechodzi do części HTML poniżej i wyświetla formularz ponownie. W polach formularza jako value/tekst ustawione są wcześniejsze wartości (ze zmiennych $imie, $email, $wiadomosc) – dzięki czemu użytkownik nie musi wpisywać od nowa wszystkiego, tylko poprawia te pola, które są błędne. Komunikaty błędów są wyświetlane w <span class=”error”> obok odpowiednich pól.

Testowanie:
Po otwarciu formularz_kontaktowy.php w przeglądarce (przez serwer PHP) powinniśmy zobaczyć formularz. Scenariusze do wypróbowania:

  • Wysłanie pustego formularza: powinny pojawić się komunikaty przy każdym polu „Proszę podać …”.
  • Wpisanie niepoprawnego email (np. „abc” lub „test@com”): powinien pojawić się błąd przy polu email o niepoprawnym formacie.
  • Poprawne wypełnienie wszystkich pól: powinna pojawić się strona z podziękowaniem i wypisanymi danymi (imię, email, wiadomość z zachowaniem nowych linii). Formularz już się nie wyświetli.
  • Częściowe błędy: np. wypełnić imię i wiadomość, zostawić pusty email – formularz pojawi się ponownie z informacją tylko o brakującym emailu, a pola imię i wiadomość zachowają wpisane wcześniej wartości.