PDO (PHP Data Objects) to wbudowany sposób komunikacji z bazą danych. Połącz się przez new PDO($dsn, $user, $password), a każde zapytanie zawierające wartość wykonuj przez prepare() i execute(), żeby wartość była wysyłana osobno od SQL:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(); // an array, or false if no row matched
Przykłady na tej stronie wymagają serwera bazy danych, więc są pokazane jako zwykły kod. Przykład z SQL injection niżej da się uruchomić.
Połączenie z MySQL przez PDO
Pierwszy argument to DSN: nazwa sterownika, a potem dane połączenia. Umieść połączenie w jednym miejscu i dołączaj je tam, gdzie go potrzebujesz.
<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // the default since PHP 8.0
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // rows as ['id' => 1, ...]
PDO::ATTR_EMULATE_PREPARES => false, // real prepared statements, real int types
];
try {
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('Database unavailable.');
}
return $pdo;
charset=utf8mb4należy do DSN. Bez tego polskie znaki, akcenty i emoji mogą przychodzić jako?albo zepsute bajty.- Nigdy nie wypisuj odwiedzającym
$e->getMessage(): może zawierać host i nazwę użytkownika. Zaloguj go. - Trzymaj hasło poza kodem, w zmiennej środowiskowej albo w pliku konfiguracyjnym poza katalogiem głównym serwera WWW.
Inne bazy danych zmieniają tylko DSN: pgsql:host=localhost;dbname=shop dla PostgreSQL, sqlite:/var/data/app.db dla pliku SQLite. Każda wymaga zainstalowanego sterownika PDO (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m wypisuje te, które masz.
Zapytania przygotowane z ? i :name
Symbol zastępczy oznacza miejsce na wartość. Używaj pozycyjnych ? z listą albo nazwanych z tablicą asocjacyjną:
<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);
// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);
// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();
Nie otaczaj symbolu zastępczego cudzysłowami: WHERE name = '?' porównuje z dosłownym tekstem ?. Zapytanie przygotowane można też wykonać wiele razy z różnymi wartościami, więc przygotuj je raz przed pętlą, a nie w jej wnętrzu.
Dlaczego sklejanie stringów w SQL jest niebezpieczne
To jedyny blok na tej stronie, który da się uruchomić: buduje zapytanie w zły i w dobry sposób dla tych samych danych. Zmień $email i uruchom ponownie.
W sklejonym zapytaniu cudzysłów w danych zamyka string, a OR '1'='1' sprawia, że warunek jest prawdziwy dla każdego wiersza: zapytanie zwraca wszystkich użytkowników. W formularzu logowania oznacza to zalogowanie bez znajomości hasła, a jeśli sterownik przyjmuje kilka instrukcji w jednym wywołaniu, dane takie jak '; DROP TABLE users; -- wykonują drugą instrukcję wybraną przez atakującego. W wersji przygotowanej baza danych dostaje SQL i wartość osobno, więc cudzysłów to tylko znak w e-mailu, który do nikogo nie pasuje.
Funkcje escapujące, takie jak addslashes(), nie są rozwiązaniem: pomijają przypadki zależne od zestawu znaków połączenia. Używaj symboli zastępczych dla każdej wartości, także liczb i wartości, które „pochodzą z twojego własnego kodu”.
Pobieranie wierszy: fetch, fetchAll i fetchColumn
<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch(); // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
http_response_code(404);
}
// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();
// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
echo $row['name'], "\n";
}
// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR); // [42 => 'Ada', ...]
query() wystarczy, gdy SQL nie zawiera żadnych wartości z zewnątrz. Gdy tylko w grę wchodzi zmienna, przejdź na prepare().
Wstawianie, aktualizacja i usuwanie
<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();
$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount(); // rows changed
$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();
lastInsertId() zwraca string, więc go rzutuj. W MySQL rowCount() po UPDATE liczy wiersze, które faktycznie się zmieniły, więc ustawienie imienia na wartość, którą już ma, daje 0. Hasła przechowuj przez password_hash, nigdy jako zwykły tekst.
Transakcje
Transakcja sprawia, że kilka instrukcji udaje się lub zawodzi razem. Jeśli cokolwiek rzuci wyjątek, wycofaj zmiany i nic nie zostanie zapisane:
<?php
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
$pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
To opiera się na tym, że błędy rzucają wyjątki (domyślnie od PHP 8.0), więc nieudana instrukcja przeskakuje do catch. W MySQL tabele muszą używać InnoDB; MyISAM ignoruje transakcje. Throwable i ponowne rzucanie opisuje strona o try/catch.
WHERE IN z tablicą
Pojedynczy ? przyjmuje pojedynczą wartość, więc IN (?) z tablicą nie działa. Zbuduj po jednym symbolu zastępczym na wartość:
<?php
$ids = [3, 8, 15];
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?')); // "?,?,?"
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$products = $stmt->fetchAll();
} else {
$products = []; // IN () would be a syntax error
}
String wstawiony do SQL to tylko znaki zapytania i przecinki, więc zapytanie nadal jest bezpieczne.
Częste błędy z symbolami zastępczymi
- Nazwy tabel i kolumn.
ORDER BY ?sortuje po stałym stringu, a nie po kolumnie. Wybieraj identyfikatory z listy dozwolonych:$sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, a potem wstaw$sortdo SQL. LIMIT ?z emulowanym przygotowaniem. Przy domyślnymATTR_EMULATE_PREPARESw MySQLexecute(['10'])wysyłaLIMIT '10', co jest błędem składni. Powiąż wartość zPDO::PARAM_INTalbo wyłącz emulację.- Ten sam nazwany symbol zastępczy dwa razy.
WHERE first = :q OR last = :qdziała tylko z emulowanym przygotowaniem. Z prawdziwym dostanieszSQLSTATE[HY093]: Invalid parameter number; użyj:q1i:q2. - Zapominanie, że
fetch()zwracafalse.$user['name']nafalsedaje ostrzeżenie inull; najpierw sprawdź wynik.
Najczęściej zadawane pytania
Jak połączyć się z MySQL przez PDO?
Utwórz obiekt PDO z DSN, użytkownikiem i hasłem: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Nieudane połączenie rzuca PDOException; złap go, zaloguj komunikat i pokaż odwiedzającemu ogólny błąd.
Czy zapytania przygotowane chronią przed SQL injection?
Tak, w przypadku wartości: z prepare('... WHERE id = ?') i execute([$id]) wartość jest wysyłana jako dane i nigdy nie może zmienić zapytania. Symbole zastępcze nie mogą oznaczać nazw tabel, nazw kolumn ani ASC/DESC, więc przed wstawieniem ich do SQL sprawdzaj je z listą dozwolonych nazw.
Czym różni się fetch od fetchAll w PDO?
fetch() zwraca następny wiersz (albo false, gdy więcej nie ma), więc pasuje do jednego wiersza albo pętli po dużym wyniku. fetchAll() zwraca naraz wszystkie wiersze w tablicy. fetchColumn() zwraca jedną wartość z następnego wiersza, co przydaje się przy SELECT COUNT(*).
Jak użyć WHERE IN z tablicą w PDO?
Zbuduj po jednym ? na wartość i przekaż tablicę do execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Najpierw sprawdź, czy tablica nie jest pusta, bo IN () to błąd składni.
Używać PDO czy mysqli?
Używaj PDO, chyba że potrzebujesz funkcji dostępnej tylko w MySQL. Jego API jest takie samo dla MySQL, PostgreSQL, SQLite i innych, obsługuje nazwane symbole zastępcze, a od PHP 8.0 domyślnie rzuca wyjątki przy błędach. mysqli działa tylko z MySQL i MariaDB.