20. PHP – które rozwiązanie wybrać? Kompleksowe porównanie – mysqli czy PDO, obiektowe czy proceduralne?


Szybki quiz – przypomnienie poprzedniej lekcji

Lekcja 19: INSERT, UPDATE, DELETE z PHP.

1. Ola ma id = 2 i dokładnie 100 XP. Co wypisze ten kod?

$xp = 100;
$id = 2;
$zapytanie = $polaczenie->prepare('UPDATE uzytkownicy SET xp = ? WHERE id = ?');
$zapytanie->bind_param('ii', $xp, $id);
$zapytanie->execute();
var_dump($zapytanie->affected_rows);

Uzasadnij wynik.

2. Znajdź błąd:

$zapytanie = $polaczenie->prepare('INSERT INTO zadania (uzytkownik_id, tytul) VALUES (?, ?)');
$zapytanie->bind_param('is', 2, $tytul);

3. Dlaczego ostatnia linia nie ma sensu?

$zapytanie = $polaczenie->prepare('DELETE FROM zadania WHERE id = ?');
$zapytanie->bind_param('i', $zadanieId);
$zapytanie->execute();
$wynik = $zapytanie->get_result();

4. Uzupełnij warunek tak, aby XP za zadanie można było dostać tylko raz i tylko za swoje zadanie:

UPDATE zadania SET data_wykonania = CURDATE()
WHERE ...

5. Skrypt dodający zadanie wykonuje INSERT, a potem od razu wypisuje HTML z komunikatem „Dodano”. Co się stanie, gdy użytkownik wciśnie F5? Jak nazywa się wzorzec, który rozwiązuje ten problem, i z jakich trzech kroków się składa?

6. Co zwraca $zapytanie->insert_id i która funkcja z czasów CSV stała się dzięki temu niepotrzebna?

7. W DELETE FROM zadania pominięto WHERE. Ile wierszy zniknie i co zwróci affected_rows?

Odpowiedzi

1. int(0). Zapytanie wykonało się poprawnie, wiersz pasował do WHERE, ale wartość się nie zmieniła (było 100, wpisujemy 100). affected_rows przy UPDATE liczy wiersze faktycznie zmienione, a nie pasujące do warunku.

2. 2 to stała, a bind_param() przyjmuje wyłącznie zmienne (przekazuje je przez referencję) — będzie Error: ... could not be passed by reference. Poprawnie: $uzytkownikId = 2; i bind_param('is', $uzytkownikId, $tytul);.

3. DELETE nie zwraca wierszy, więc nie ma czego odbierać przez get_result(). Efekt sprawdzamy przez $zapytanie->affected_rows.

4. WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL — pierwszy warunek wskazuje zadanie, drugi pilnuje właściciela (id z formularza można podrobić, $_SESSION['uzytkownik_id'] nie), trzeci sprawia, że drugie kliknięcie zmieni 0 wierszy i XP nie wpadnie dwa razy.

5. F5 powtarza żądanie POST, więc INSERT wykona się ponownie i powstanie duplikat. Rozwiązanie to PRG (Post/Redirect/Get): POST przetwarza dane → header('Location: ...') + exit → przeglądarka pobiera stronę metodą GET, więc odświeżenie nie powtarza zapisu.

6. Zwraca id nadane właśnie dodanemu wierszowi przez AUTO_INCREMENT. Zastąpiło nastepneIdZadania(), które przy CSV musiało samo wyliczać kolejny numer.

7. Zniknie cała zawartość tabeli, a affected_rows pokaże liczbę usuniętych wierszy. WHERE jest jedynym bezpiecznikiem — baza nie zapyta „na pewno?”.


Dodatek

PDO (PHP Data Objects) – ZALECANE dla większości projektów

PDO to obecnie najlepszy wybór dla nowoczesnych aplikacji PHP ze względu na:

  • Uniwersalność (możliwość zmiany bazy danych)
  • Czytelność kodu (named parameters)
  • Bezpieczeństwo (domyślne prepared statements)
  • Zgodność ze standardami branżowymi
  • Wsparcie dla frameworków (Laravel, Symfony używają PDO)

Szczegółowe porównanie tabelaryczne

KryteriumPDOMySQLi (obiektowo)MySQLi (proceduralnie)
Obsługiwane bazy danychMySQL, PostgreSQL, SQLite, Oracle, MS SQL ServerTylko MySQL/MariaDBTylko MySQL/MariaDB
Styl programowaniaTylko obiektowy (nowoczesny)ObiektowyProceduralny (przestarzały)
Prepared statementsNamed + positionalTylko positionalTylko positional
Named parameters:email, :id (czytelne)Tylko ?Tylko ?
Obsługa błędówExceptions (nowoczesne)Exceptions + kodyGłównie kody błędów
TransakcjebeginTransaction(), commit(), rollback()begin_transaction(), commit(), rollback()mysqli_begin_transaction()
WydajnośćBardzo dobraNieznacznie szybsza (1-2%)Nieznacznie szybsza
Czytelność koduBardzo wysokaWysokaNiska
Popularność w 202460% projektów35% projektów5% projektów (legacy)
FrameworkiLaravel, Symfony, Slim, YiiRzadko używaneNie używane
Wsparcie społecznościBardzo dużeDużeMałe
Łatwość naukiBardzo łatweŁatweŁatwe ale przestarzałe
Przenośność koduWysoka (zmiana bazy bez przepisywania)Niska (tylko MySQL)Niska
Zgodność z OOPW pełni obiektoweObiektoweNie obiektowe
Multiple queriesNie wspiera (bezpieczniejsze)WspieraWspiera
Asynchroniczne zapytaniaNie wspieraWspieraWspiera
Polecane dlaNowe projekty, aplikacje komercyjneProjekty tylko MySQLStare projekty (nie polecane)

Porównanie kodu – Przykłady praktyczne

Przykład 1: Połączenie z bazą

PDO (zalecane):

try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=app_db;charset=utf8mb4',
        'username',
        'password',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
    echo "Połączono z bazą!";
} catch (PDOException $e) {
    die("Błąd: " . $e->getMessage());
}

MySQLi (obiektowo):

$mysqli = new mysqli('localhost', 'username', 'password', 'app_db');

if ($mysqli->connect_error) {
    die("Błąd: " . $mysqli->connect_error);
}

$mysqli->set_charset('utf8mb4');
echo "Połączono z bazą!";

MySQLi (proceduralnie) – NIE POLECANE:

$conn = mysqli_connect('localhost', 'username', 'password', 'app_db');

if (!$conn) {
    die("Błąd: " . mysqli_connect_error());
}

mysqli_set_charset($conn, 'utf8mb4');
echo "Połączono z bazą!";

Werdykt: PDO wygrywa – zwięzłe, z automatyczną obsługą wyjątków.

Przykład 2: Prepared Statements (bezpieczeństwo)

PDO (zalecane) – Named Parameters:

// Bardzo czytelne - wiadomo co jest czym
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND active = :active");
$stmt->execute([
    'email' => $email,
    'active' => 1
]);
$user = $stmt->fetch();

MySQLi (obiektowo):

// Mniej czytelne - trzeba pamiętać kolejność
$stmt = $mysqli->prepare("SELECT * FROM users WHERE email = ? AND active = ?");
$aktywny = 1;
$stmt->bind_param("si", $email, $aktywny);  // s=string, i=integer
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();

MySQLi (proceduralnie) – NIE POLECANE:

// Najbardziej rozwlekłe
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE email = ? AND active = ?");
$aktywny = 1;
mysqli_stmt_bind_param($stmt, "si", $email, $aktywny);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);

Przykład 3: INSERT z pobieraniem ID

PDO (zalecane):

$stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (:username, :email)");
$stmt->execute([
    'username' => $username,
    'email' => $email
]);
$userId = $pdo->lastInsertId();
echo "Dodano użytkownika ID: $userId";

MySQLi (obiektowo):

$stmt = $mysqli->prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $email);
$stmt->execute();
$userId = $mysqli->insert_id;
echo "Dodano użytkownika ID: $userId";

MySQLi (proceduralnie) – NIE POLECANE:

$stmt = mysqli_prepare($conn, "INSERT INTO users (username, email) VALUES (?, ?)");
mysqli_stmt_bind_param($stmt, "ss", $username, $email);
mysqli_stmt_execute($stmt);
$userId = mysqli_insert_id($conn);
echo "Dodano użytkownika ID: $userId";

Przykład 4: Transakcje

PDO (zalecane):

try {
    $pdo->beginTransaction();
    
    $pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
    
    $pdo->commit();
    echo "Przelew wykonany!";
} catch (Exception $e) {
    $pdo->rollBack();
    echo "Błąd: " . $e->getMessage();
}

MySQLi (obiektowo):

$mysqli->begin_transaction();

try {
    $mysqli->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $mysqli->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
    
    $mysqli->commit();
    echo "Przelew wykonany!";
} catch (Exception $e) {
    $mysqli->rollback();
    echo "Błąd: " . $e->getMessage();
}

MySQLi (proceduralnie) – NIE POLECANE:

mysqli_begin_transaction($conn);

$success = mysqli_query($conn, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$success = $success && mysqli_query($conn, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");

if ($success) {
    mysqli_commit($conn);
    echo "Przelew wykonany!";
} else {
    mysqli_rollback($conn);
    echo "Błąd: " . mysqli_error($conn);
}

Kiedy używać którego rozwiązania?

  • Budujesz nowy projekt (zawsze!)
  • Chcesz mieć możliwość zmiany bazy danych w przyszłości
  • Pracujesz nad aplikacją komercyjną
  • Uczysz się nowoczesnego PHP
  • Chcesz kodu zgodnego z branżowymi standardami
  • Planujesz używać frameworków (Laravel, Symfony)
  • Cenisz czytelność kodu (named parameters)

Przykłady projektów:

  • System CRM
  • E-commerce
  • Blog/CMS
  • API REST
  • Aplikacja webowa SaaS
  • Panel Discord bota
  • System zarządzania treningami

Używaj MySQLi (obiektowo) gdy:

  • Na 100% będziesz używać tylko MySQL/MariaDB
  • Potrzebujesz asynchronicznych zapytań
  • Potrzebujesz multiple statements
  • Pracujesz z istniejącym projektem używającym MySQLi
  • Wydajność jest KRYTYCZNA (różnica ~1-2%)

Przykłady projektów:

  • Wysoko obciążony system (miliony zapytań/dzień)
  • Integracje wymagające multiple queries
  • Migracja starego projektu z mysql do mysqli

Używaj MySQLi (proceduralnie) TYLKO gdy:

  • Pracujesz z bardzo starym kodem legacy
  • Musisz utrzymywać stary projekt

NIE UŻYWAJ w nowych projektach! To przestarzałe podejście.

PODSUMOWANIE LEKCJI

W skrócie

Ta lekcja nie wprowadza nowej funkcjonalności — porządkuje wybór narzędzia. Najważniejsze do zrozumienia:

  • mysqli i PDO robią to samo: łączą PHP z bazą, wysyłają zapytania, odbierają wyniki. Różnią się zasięgiem i składnią, a nie możliwościami.
  • Zasięg: mysqli działa tylko z MySQL/MariaDB. PDO obsługuje wiele baz — zmienia się DSN (mysql:, pgsql:, sqlite:), ale samo SQL i tak trzeba sprawdzić, bo dialekty się różnią. „Przenośność” nie znaczy „kod zadziała bez zmian”.
  • Proceduralnie vs obiektowo: mysqli_query($conn, ...) i $conn->query(...) to ta sama biblioteka w dwóch stylach. Nowego kodu nie piszemy proceduralnie — wersję proceduralną trzeba jednak umieć przeczytać, bo pełno jej w starych poradnikach i przykładach w sieci.
  • Bezpieczeństwo nie zależy od biblioteki. Zapytanie sklejone ze stringów jest tak samo podatne na SQL injection w PDO, jak w mysqli. Chronią prepared statements, a nie wybór rozszerzenia.
  • Obsługa błędów wymaga ustawienia w obu: w PDO PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, w mysqli mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT). W nowszych wersjach PHP oba zachowują się tak domyślnie, ale piszemy je jawnie — kod nie zależy wtedy od konfiguracji serwera.
  • Nie mieszamy bibliotek w jednym projekcie. Obiekt PDO nie ma metody bind_param(), a obiekt mysqli nie ma fetchAll(). TaskQuest od Lekcji 17 korzysta z mysqli obiektowo i tak zostaje — rekomendacja PDO nie oznacza, że mysqli jest błędem.
  • Kryteria realnego wyboru (w tej kolejności): czego używa istniejący kod projektu → czy na pewno tylko MySQL → czego wymaga framework → co zespół zna najlepiej. Wydajność w praktyce nie rozstrzyga.

Typowe źródła błędów przy przesiadce z jednego na drugie:

  • w mysqli bind_param() przyjmuje tylko zmienne (nie stałe, nie wyrażenia) i liczba liter typu musi równać się liczbie ?,
  • w PDO z ATTR_EMULATE_PREPARES => false wartość do LIMIT trzeba związać jako liczbę (bindValue(':limit', $limit, PDO::PARAM_INT)), bo domyślnie pójdzie jako tekst i zapytanie się wywali,
  • get_result() istnieje tylko w mysqli; w PDO wynik odbiera się prosto ze $stmt.

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. mysqli = tylko MySQL/MariaDB. PDO = wiele baz. To jest główna różnica.
  2. Wersja proceduralna (mysqli_*) — tylko do czytania starego kodu. Nie piszesz w niej nowych projektów.
  3. Prepared statements są w obu i to one dają bezpieczeństwo, nie sam wybór biblioteki.
  4. PDO ma parametry nazwane :email, mysqli tylko ? z literami typów (i, d, s, b).
  5. Zamknięcie połączenia: $polaczenie->close() (mysqli) / $pdo = null (PDO) — na egzaminie wymagane jawnie.
  6. Jeden projekt = jedna biblioteka. Mieszanie kończy się błędem „Call to undefined method”.
  7. TaskQuest używa mysqli obiektowo — i przy tym zostajemy do końca kursu.

Ściągawka – najważniejsze polecenia i składnia

Te same czynności w obu bibliotekach

Czynność mysqli (obiektowo) PDO
Połączenie new mysqli($host, $user, $pass, $baza) new PDO("mysql:host=$host;dbname=$baza;charset=utf8mb4", $user, $pass)
Kodowanie $db->set_charset('utf8mb4') w DSN: charset=utf8mb4
Błędy jako wyjątki mysqli_report(MYSQLI_REPORT_ERROR \| MYSQLI_REPORT_STRICT) PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
Klasa wyjątku mysqli_sql_exception PDOException
Przygotowanie $st = $db->prepare('... WHERE id = ?') $st = $pdo->prepare('... WHERE id = :id')
Podstawienie danych $st->bind_param('i', $id) + $st->execute() $st->execute(['id' => $id])
Wynik SELECT $wynik = $st->get_result() wprost ze $st
Jeden wiersz $wynik->fetch_assoc() $st->fetch()
Wszystkie wiersze $wynik->fetch_all(MYSQLI_ASSOC) $st->fetchAll()
Liczba wierszy SELECT $wynik->num_rows count($st->fetchAll())
Zmienione wiersze $st->affected_rows $st->rowCount()
Id po INSERT $st->insert_id $pdo->lastInsertId()
Transakcje begin_transaction() / commit() / rollback() beginTransaction() / commit() / rollBack()
Zamknięcie $db->close() $pdo = null;

Szkielet – mysqli obiektowo

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'nazwa_bazy');
$polaczenie->set_charset('utf8mb4');

$zapytanie = $polaczenie->prepare('SELECT id, nazwa FROM obiekty WHERE idKontynent = ?');
$zapytanie->bind_param('i', $idKontynent);
$zapytanie->execute();
$wynik = $zapytanie->get_result();

while ($wiersz = $wynik->fetch_assoc()) {
    echo '<p>' . $wiersz['nazwa'] . '</p>';
}

$polaczenie->close();

Szkielet – PDO

$pdo = new PDO('mysql:host=localhost;dbname=nazwa_bazy;charset=utf8mb4', 'root', '', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$zapytanie = $pdo->prepare('SELECT id, nazwa FROM obiekty WHERE idKontynent = :id');
$zapytanie->execute(['id' => $idKontynent]);

foreach ($zapytanie as $wiersz) {
    echo '<p>' . $wiersz['nazwa'] . '</p>';
}

$pdo = null;

Rozpoznawanie kodu na pierwszy rzut oka

  • mysqli_connect(, mysqli_query(, $conn jako pierwszy argument → mysqli proceduralnie (stary kod),
  • ->bind_param('si', ...), ->get_result(), ->num_rows → mysqli obiektowo,
  • new PDO(, :nazwa, ->fetchAll(), ->rowCount() → PDO.

Quiz – sprawdź się

1. Z której biblioteki pochodzi każdy fragment?

// A
$w = $st->fetchAll();
// B
$w = $wynik->fetch_all(MYSQLI_ASSOC);
// C
$w = mysqli_fetch_assoc($wynik);

2. Przepisz to zapytanie na PDO (z parametrem nazwanym):

$st = $polaczenie->prepare('SELECT nick FROM turysci WHERE idTurysta = ?');
$st->bind_param('i', $idTurysty);
$st->execute();

3. Dlaczego ten kod się wywali?

$pdo = new PDO('mysql:host=localhost;dbname=wodospady', 'root', '');
$st = $pdo->prepare('SELECT * FROM obiekty WHERE idRodzaj = ?');
$st->bind_param('i', $idRodzaj);

4. Programista twierdzi: „Przepisałem projekt z mysqli na PDO, więc jest teraz odporny na SQL injection”. Czy ma rację? Uzasadnij jednym zdaniem.

5. Co trzeba dopisać na końcu skryptu egzaminacyjnego w każdej z wersji, żeby spełnić wymaganie „na końcu należy zamknąć połączenie z bazą”?

6. Aplikacja ma docelowo działać na MySQL, ale klient zapowiada możliwą migrację na PostgreSQL. Którą bibliotekę wybierasz i czego mimo to nie załatwi za Ciebie ten wybór?

7. Uzupełnij brakujący element, żeby kod działał:

$st = $pdo->prepare('INSERT INTO osiagniecia (idObiekt, idTurysta) VALUES (:obiekt, :turysta)');
$st->execute([________]);
$noweId = ________;

8. Znajdź błąd:

$polaczenie = new mysqli('localhost', 'root', '', 'wodospady');
$st = $polaczenie->prepare('UPDATE turysci SET nick = ? WHERE idTurysta = ?');
$st->bind_param('s', $nick, $id);
$st->execute();

9. Który z tych zapisów kończy pracę z bazą w PDO: close(), = null, disconnect(), unset()? Wskaż wszystkie poprawne.

10. Krótkie zadanie praktyczne: masz gotowy skrypt w mysqli obiektowym wypisujący listę kontynentów w pętli while. Wymień cztery linie/elementy, które trzeba zmienić, żeby przenieść go na PDO.


Odpowiedzi

1. A – PDO, B – mysqli obiektowo, C – mysqli proceduralnie.

2.

$st = $pdo->prepare('SELECT nick FROM turysci WHERE idTurysta = :id');
$st->execute(['id' => $idTurysty]);

3. bind_param() to metoda mysqli — PDO jej nie ma (Call to undefined method PDOStatement::bind_param()). W PDO dane podaje się w execute([...]) albo przez bindValue(). Dodatkowo brakuje tu charset=utf8mb4 w DSN i trybu wyjątków.

4. Nie. Bezpieczeństwo daje stosowanie prepared statements i parametrów — jeśli w PDO dalej skleja zapytanie ze zmiennych ("... WHERE id = $id"), podatność zostaje.

5. mysqli: $polaczenie->close(); PDO: $pdo = null;

6. PDO — zmiana bazy sprowadza się do zmiany DSN i sterownika. Nie załatwi za Ciebie różnic w samym SQL (typy kolumn, funkcje, LIMIT/AUTO_INCREMENT vs odpowiedniki PostgreSQL) — zapytania i tak trzeba przejrzeć.

7.

$st->execute(['obiekt' => $idObiektu, 'turysta' => $idTurysty]);
$noweId = $pdo->lastInsertId();

8. W zapytaniu są dwa ?, a w bind_param() tylko jedna litera typu ('s'). Powinno być bind_param('si', $nick, $id). Brakuje też set_charset('utf8mb4') i zamknięcia połączenia.

9. Poprawne: $pdo = null; oraz unset($pdo);. close() należy do mysqli, disconnect() nie istnieje.

10. Na przykład: (1) new mysqli(...) → new PDO('mysql:host=...;dbname=...;charset=utf8mb4', ...) z opcjami; (2) usunięcie set_charset() (kodowanie jest w DSN); (3) bind_param() + execute() → execute([...]) z parametrem nazwanym; (4) get_result() + while ($w = $wynik->fetch_assoc()) → foreach ($st as $w) albo $st->fetchAll(); (5) close() → = null.