PDO (PHP Data Objects) это встроенный способ работать с базой данных. Подключитесь через new PDO($dsn, $user, $password), а затем выполняйте любой запрос, где есть значение, через prepare() и execute(), чтобы значение отправлялось отдельно от 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
Примерам на этой странице нужен сервер базы данных, поэтому они показаны обычным кодом. Пример с SQL-инъекцией ниже запускается.
Подключение к MySQL через PDO
Первый аргумент это DSN: имя драйвера, а за ним параметры подключения. Держите подключение в одном месте и подключайте его там, где оно нужно.
<?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должен быть в DSN. Без него кириллица, диакритика и эмодзи могут приходить как?или испорченные байты.- Никогда не выводите
$e->getMessage()посетителям: там могут быть хост и имя пользователя. Пишите его в лог. - Держите пароль вне кода, в переменной окружения или файле настроек за пределами корня сайта.
Для других баз данных меняется только DSN: pgsql:host=localhost;dbname=shop для PostgreSQL, sqlite:/var/data/app.db для файла SQLite. Для каждой нужен установленный драйвер PDO (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m показывает, какие у вас есть.
Подготовленные запросы с ? и :name
Плейсхолдер отмечает, куда идёт значение. Используйте позиционные плейсхолдеры ? со списком или именованные с ассоциативным массивом:
<?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();
Не ставьте кавычки вокруг плейсхолдера: WHERE name = '?' сравнивает с буквальным текстом ?. Подготовленный запрос можно выполнить много раз с разными значениями, поэтому готовьте его один раз до цикла, а не внутри него.
Почему склеивать SQL из строк опасно
Это единственный блок на странице, который запускается: он строит запрос неправильным и правильным способом для одного и того же ввода. Измените $email и запустите снова.
В склеенном запросе кавычка из ввода закрывает строку, а OR '1'='1' делает условие истинным для каждой строки: запрос возвращает всех пользователей. На форме входа это значит войти, не зная пароля, а если драйвер принимает несколько инструкций за один вызов, ввод вроде '; DROP TABLE users; -- выполняет вторую инструкцию по выбору атакующего. В подготовленном варианте база данных получает SQL и значение отдельно, поэтому кавычка это просто символ внутри email, который ни с кем не совпадает.
Функции экранирования вроде addslashes() не решение: они пропускают случаи, которые зависят от кодировки соединения. Используйте плейсхолдеры для каждого значения, включая числа и значения, которые "приходят из вашего собственного кода".
Получение строк: fetch, fetchAll и 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() подходит, когда в SQL нет значений извне. Как только появляется переменная, переходите на prepare().
Вставка, обновление и удаление
<?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() возвращает строку, поэтому приводите её к int. В MySQL rowCount() после UPDATE считает строки, которые действительно изменились, поэтому присвоение имени того же значения, что уже было, даёт 0. Храните пароли через password_hash, никогда открытым текстом.
Транзакции
Транзакция заставляет несколько инструкций выполниться или не выполниться вместе. Если что-то выбрасывает исключение, сделайте откат, и ничего не сохранится:
<?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;
}
Это опирается на то, что ошибки выбрасывают исключения (по умолчанию начиная с PHP 8.0), поэтому неудачная инструкция переходит в catch. В MySQL таблицы должны использовать InnoDB; MyISAM игнорирует транзакции. Throwable и повторный выброс описаны на странице про try/catch.
WHERE IN с массивом
Один ? вмещает одно значение, поэтому IN (?) с массивом не работает. Соберите по плейсхолдеру на значение:
<?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
}
Строка, вставленная в SQL, состоит только из вопросительных знаков и запятых, поэтому запрос остаётся безопасным.
Частые ошибки с плейсхолдерами
- Имена таблиц и столбцов.
ORDER BY ?сортирует по константной строке, а не по столбцу. Выбирайте идентификаторы из списка разрешённых:$sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, затем вставляйте$sortв SQL. LIMIT ?с эмуляцией подготовки. С включённым по умолчанию в MySQLATTR_EMULATE_PREPARESexecute(['10'])отправляетLIMIT '10', а это синтаксическая ошибка. Привяжите значение сPDO::PARAM_INTили выключите эмуляцию.- Один и тот же именованный плейсхолдер дважды.
WHERE first = :q OR last = :qработает только с эмуляцией подготовки. С настоящей подготовкой вы получитеSQLSTATE[HY093]: Invalid parameter number; используйте:q1и:q2. - Забыть, что
fetch()возвращаетfalse.$user['name']дляfalseдаёт предупреждение иnull; сначала проверяйте результат.
Часто задаваемые вопросы
Как подключиться к MySQL через PDO?
Создайте объект PDO с DSN, пользователем и паролем: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Неудачное подключение выбрасывает PDOException; перехватите его, запишите сообщение в лог и покажите посетителю общую ошибку.
Защищают ли подготовленные запросы от SQL-инъекций?
Да, для значений: с prepare('... WHERE id = ?') и execute([$id]) значение отправляется как данные и никогда не может изменить запрос. Плейсхолдеры не могут заменять имена таблиц, столбцов или ASC/DESC, поэтому проверяйте их по списку разрешённых имён, прежде чем вставлять в SQL.
Чем fetch отличается от fetchAll в PDO?
fetch() возвращает следующую строку (или false, когда строк больше нет), поэтому подходит для одной строки или цикла по большому результату. fetchAll() возвращает все строки массивом сразу. fetchColumn() возвращает одно значение из следующей строки, что удобно для SELECT COUNT(*).
Как использовать WHERE IN с массивом в PDO?
Соберите по одному ? на значение и передайте массив в execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Сначала проверьте, что массив не пуст, потому что IN () это синтаксическая ошибка.
Что использовать: PDO или mysqli?
Используйте PDO, если не нужна возможность, которая есть только в MySQL. Его API одинаков для MySQL, PostgreSQL, SQLite и других баз, он поддерживает именованные плейсхолдеры, а начиная с PHP 8.0 по умолчанию выбрасывает исключения при ошибках. mysqli работает только с MySQL и MariaDB.