Перейти к основному содержимому

PDO в PHP — подключение и безопасные запросы

Разработчику Архитектору

PDO (PHP Data Objects) — единый интерфейс доступа к разным СУБД через драйверы (pdo_mysql, pdo_pgsql, pdo_sqlite). Это выжимка для старта; расширенный разбор ORM, MySQLi и оптимизации — в Работа с базами данных из PHP.


Подключение

<?php
declare(strict_types=1);

$dsn = 'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4';
$user = 'app_user';
$pass = 'secret';

$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
ПараметрНазначение
ERRMODE_EXCEPTIONОшибки SQL → PDOException, а не тихий сбой
FETCH_ASSOCСтроки как ассоциативные массивы
EMULATE_PREPARES falseНастоящие prepared statements на стороне MySQL

DSN для SQLite (удобно для учебных проектов без сервера MySQL):

$pdo = new PDO('sqlite:' . __DIR__ . '/data/app.sqlite', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

Учётные данные хранят в переменных окружения или .env, не в репозитории.


Подготовленные запросы

Плейсхолдеры ? или именованные :name отделяют структуру SQL от данных. Это основная защита от SQL-инъекций.

$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();

if ($user === false) {
// не найден
}

Вставка:

$stmt = $pdo->prepare(
'INSERT INTO users (email, name, created_at) VALUES (:email, :name, :created_at)'
);
$stmt->execute([
'email' => $email,
'name' => $name,
'created_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
]);
$newId = (int) $pdo->lastInsertId();

Нельзя подставлять имена таблиц и колонок через плейсхолдеры — только значения. Для динамических имён — белый список в коде.


Выборка нескольких строк

$stmt = $pdo->prepare('SELECT id, title FROM posts WHERE user_id = :uid ORDER BY id DESC');
$stmt->execute(['uid' => $userId]);
$posts = $stmt->fetchAll();

Итерация без загрузки всего результата в память (большие таблицы):

$stmt = $pdo->query('SELECT id, body FROM logs'); // только доверенный SQL без пользовательских частей
while ($row = $stmt->fetch()) {
processLogLine($row);
}

Обновление и удаление

$stmt = $pdo->prepare('UPDATE users SET name = :name WHERE id = :id');
$stmt->execute(['name' => $name, 'id' => $id]);
$updated = $stmt->rowCount();

$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < :now');
$stmt->execute(['now' => date('Y-m-d H:i:s')]);

rowCount() для SELECT в MySQL может вести себя неинтуитивно; для проверки «есть ли строка» надёжнее fetch().


Транзакции

Группа операций «всё или ничего»:

$pdo->beginTransaction();
try {
$pdo->prepare('UPDATE accounts SET balance = balance - :sum WHERE id = :id')
->execute(['sum' => $sum, 'id' => $fromId]);
$pdo->prepare('UPDATE accounts SET balance = balance + :sum WHERE id = :id')
->execute(['sum' => $sum, 'id' => $toId]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}

При ERRMODE_EXCEPTION сбой execute прерывает блок и уходит в catch.


Обработка ошибок

try {
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// ...
} catch (PDOException $e) {
error_log('Database: ' . $e->getMessage());
http_response_code(503);
echo 'Сервис временно недоступен';
}

Детали подключения (пароль, хост) не показывают пользователю. См. Исключения в прикладном коде.


PDO и MySQLi

PDOMySQLi
СУБДMySQL, PostgreSQL, SQLite, …В основном MySQL
СтильОдин API для всехПроцедурный и ООП
Подготовленные запросыДаДа

Для новых проектов на MySQL чаще выбирают PDO из-за единообразия и переносимости. MySQLi остаётся в легаси и узкоспециализированных случаях — см. полный раздел по БД.


Чек-лист перед продакшеном

  1. ATTR_ERRMODE = EXCEPTION
  2. Все пользовательские значения — только через prepare + execute
  3. Кодировка utf8mb4 в DSN MySQL
  4. Отдельный пользователь БД с минимальными правами (не root)
  5. Пароль и DSN — из окружения, не из git

Что изучить дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).