PDO в PHP — подключение и безопасные запросы
PDO (PHP Data Objects) — единый интерфейс доступа к разным СУБД через драйверы (pdo_mysql, pdo_pgsql, pdo_sqlite). Это выжимка для старта; расширенный разбор ORM, MySQLi и оптимизации — в Работа с базами данных из PHP.
Главная идея PDO: вы пишете одинаковый прикладной код для запросов, а различия между СУБД остаются в SQL и DSN.
Подключение
<?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,
]);
Разбор:
declare(strict_types=1);включает строгую проверку скалярных типов для вызовов функций из этого файла.- Строка
DSNзадает драйвер, хост, имя базы и кодировкуutf8mb4, чтобы корректно хранить Unicode и emoji. - Конструктор
new PDO(...)открывает соединение с БД и возвращает объект доступа к запросам. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTIONпереводит SQL-ошибки в исключения, чтобы их можно было перехватывать черезtry/catch.PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOCзадает удобный формат выборки строк как ассоциативных массивов.PDO::ATTR_EMULATE_PREPARES => falseзаставляет использовать нативные prepared statements драйвера, что улучшает предсказуемость и безопасность.
| Параметр | Назначение |
|---|---|
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,
]);
Разбор:
- Префикс
sqlite:в DSN означает подключение к файловой базе SQLite без отдельного серверного процесса. __DIR__дает абсолютный путь к каталогу текущего файла, поэтому путь к БД не зависит от текущей рабочей директории процесса.- Именованный аргумент
options:передает массив атрибутов подключения, как и в варианте с MySQL. - Единственный атрибут
ERRMODE_EXCEPTIONсохраняет единый подход к обработке ошибок для разных СУБД. - Такой формат удобен для локальной разработки и учебных сценариев, где важно быстро поднять окружение.
Учётные данные хранят в переменных окружения или .env, не в репозитории.
Подготовленные запросы
Плейсхолдеры ? или именованные :name отделяют структуру SQL от данных. Это основная защита от SQL-инъекций.
Полезный практический шаблон:
- SQL-строка хранится отдельно и не собирается конкатенацией.
- Все пользовательские значения передаются вторым шагом в
execute. - Любая ошибка обработки уходит в исключение и фиксируется логированием.
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
if ($user === false) {
// не найден
}
Разбор:
$pdo->prepare(...)компилирует SQL-шаблон с именованным параметром:email.execute(['email' => $email])безопасно привязывает значение к плейсхолдеру и запускает запрос.fetch()читает одну строку результата в формате, заданномFETCH_ASSOC.- Проверка
$user === falseкорректно различает состояние "строка не найдена" и "строка найдена". LIMIT 1ограничивает результат одной записью и уменьшает лишнюю работу БД.
Вставка:
$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();
Разбор:
prepare(...)отделяет структуруINSERTот пользовательских данных.execute([...])передает значения полей и исключает ручную конкатенацию SQL-строк.(new DateTimeImmutable())->format('Y-m-d H:i:s')формирует timestamp в формате, ожидаемомDATETIME.lastInsertId()возвращает идентификатор только что вставленной строки для текущего соединения.- Явное приведение
(int)делает тип идентификатора предсказуемым в прикладном коде.
Нельзя подставлять имена таблиц и колонок через плейсхолдеры — только значения. Для динамических имён — белый список в коде.
Выборка нескольких строк
$stmt = $pdo->prepare('SELECT id, title FROM posts WHERE user_id = :uid ORDER BY id DESC');
$stmt->execute(['uid' => $userId]);
$posts = $stmt->fetchAll();
Разбор:
- Запрос выбирает посты только одного пользователя по параметру
:uid. ORDER BY id DESCсортирует выборку от новых записей к старым.fetchAll()загружает весь результат сразу в память как массив строк.- Этот вариант удобен для небольших выборок, которые нужно показать целиком на странице.
- Для больших наборов данных лучше переходить на поштучное чтение через
fetch()в цикле.
Итерация без загрузки всего результата в память (большие таблицы):
$stmt = $pdo->query('SELECT id, body FROM logs'); // только доверенный SQL без пользовательских частей
while ($row = $stmt->fetch()) {
processLogLine($row);
}
Разбор:
query(...)выполняет SQL сразу, без отдельного шагаprepare/execute.- Этот подход допустим только для полностью статичного, доверенного SQL без пользовательского ввода.
fetch()внутриwhileчитает строки по одной и снижает пиковое потребление памяти.- Каждая строка сразу передается в
processLogLine($row), что удобно для потоковой обработки логов. - Такой шаблон полезен для batch-задач и фоновых скриптов.
Обновление и удаление
$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')]);
Разбор:
- Первый блок обновляет имя конкретного пользователя по
idчерез безопасные параметры. rowCount()послеUPDATEпоказывает число затронутых строк и помогает проверить эффект операции.- Второй блок удаляет все просроченные сессии по сравнению с текущим временем сервера.
date('Y-m-d H:i:s')формирует строку в формате, совместимом с типомDATETIME.- Оба запроса остаются инъекционно-безопасными за счет
prepare + execute.
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;
}
Разбор:
beginTransaction()открывает транзакцию и откладывает окончательную фиксацию изменений.- Первый
UPDATEсписывает сумму у отправителя, второйUPDATEзачисляет сумму получателю. commit()фиксирует обе операции как единый атомарный шаг.- При любой ошибке управление переходит в
catch, где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 'Сервис временно недоступен';
}
Разбор:
tryоборачивает подключение и рабочие операции, где может возникнутьPDOException.- В
catchошибка логируется черезerror_log(...), чтобы разработчик увидел детали в серверных логах. http_response_code(503)возвращает корректный статус временной недоступности сервиса.- Пользователю выводится нейтральное сообщение без технических деталей, паролей и DSN.
- Такой шаблон разделяет внутреннюю диагностику и безопасный внешний ответ.
Детали подключения (пароль, хост) не показывают пользователю. См. Исключения в прикладном коде.
PDO и MySQLi
| PDO | MySQLi | |
|---|---|---|
| СУБД | MySQL, PostgreSQL, SQLite, … | В основном MySQL |
| Стиль | Один API для всех | Процедурный и ООП |
| Подготовленные запросы | Да | Да |
Для новых проектов на MySQL чаще выбирают PDO из-за единообразия и переносимости. MySQLi остаётся в легаси и узкоспециализированных случаях — см. полный раздел по БД.
Чек-лист перед продакшеном
ATTR_ERRMODE = EXCEPTION- Все пользовательские значения — только через
prepare+execute - Кодировка
utf8mb4в DSN MySQL - Отдельный пользователь БД с минимальными правами (не
root) - Пароль и DSN — из окружения, не из git
Что изучить дальше
- От формы до записи в БД — сквозной пример
- Работа с данными со страницы
- Работа с базами данных из PHP
- Исключения
- Загрузка файлов и валидация