Menu

PDO w PHP: połączenie z MySQL i zapytania przygotowane

PDO to warstwa bazy danych PHP: połącz się przez new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass), a każde zapytanie z wartościami wykonuj przez prepare() i execute([$value]). Poznaj łączenie, zapytania przygotowane, pobieranie wierszy, wstawianie, transakcje i WHERE IN.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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=utf8mb4 należ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 $sort do SQL.
  • LIMIT ? z emulowanym przygotowaniem. Przy domyślnym ATTR_EMULATE_PREPARES w MySQL execute(['10']) wysyła LIMIT '10', co jest błędem składni. Powiąż wartość z PDO::PARAM_INT albo wyłącz emulację.
  • Ten sam nazwany symbol zastępczy dwa razy. WHERE first = :q OR last = :q działa tylko z emulowanym przygotowaniem. Z prawdziwym dostaniesz SQLSTATE[HY093]: Invalid parameter number; użyj :q1 i :q2.
  • Zapominanie, że fetch() zwraca false. $user['name'] na false daje ostrzeżenie i null; 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ