Что такое PDO
PDO, PHP Data Objects, — это стандартный интерфейс PHP для работы с базами данных. Он даёт один набор классов и методов: PDO для соединения, PDOStatement для подготовленного запроса или результата, PDOException для ошибок. Важная граница: PDO не делает SQL универсальным. Он не переписывает LIMIT, RETURNING, JSON-операторы или типы данных между MySQL, PostgreSQL и SQLite. Это слой доступа к данным, а не полноценный query builder и не ORM.
Поэтому PDO хорошо отвечает на вопрос «как из PHP безопасно подключиться, выполнить запрос и получить строки», но не отменяет знания конкретной СУБД. Тема тесно связана с Prepared statements и SQL injection: именно через prepare() и execute() PDO чаще всего используют для безопасной передачи значений в SQL.
flowchart TD
A[PHP-код] --> B[PDO]
B --> C[PDO-драйвер: pdo_mysql / pdo_pgsql / pdo_sqlite]
C --> D[(База данных)]
B --> E[PDOStatement]
E --> F[execute / fetch / fetchAll]
A --> G[DSN + user + password + options]
G --> BDSN и драйверы
Соединение создаётся через конструктор:
<?php
$pdo = new PDO($dsn, $username, $password, $options);$dsn — Data Source Name, строка с именем PDO-драйвера и параметрами подключения. Драйвер — это расширение для конкретной базы: pdo_mysql, pdo_pgsql, pdo_sqlite, pdo_oci, pdo_sqlsrv и так далее. Сам PDO без драйвера не умеет говорить с сервером базы.
Проверить доступные драйверы можно так:
<?php
print_r(PDO::getAvailableDrivers());Типичные DSN выглядят по-разному:
<?php
$mysql = 'mysql:host=127.0.0.1;port=3306;dbname=app;charset=utf8mb4';
$pgsql = 'pgsql:host=127.0.0.1;port=5432;dbname=app';
$sqlite = 'sqlite:' . __DIR__ . '/database.sqlite';
$memory = 'sqlite::memory:';Для MySQL почти всегда стоит указывать charset=utf8mb4 прямо в DSN. Это не украшение: кодировка соединения влияет на то, как база принимает и отдаёт строки. Если приложение хранит пользовательский текст, связь с Строки, UTF-8 и mbstring здесь прямая: байты должны одинаково пониматься PHP-кодом, драйвером и базой.
Отдельная практическая деталь MySQL: localhost на Unix-системах может означать подключение через Unix socket, а 127.0.0.1 — TCP. Когда в Docker или на сервере «всё правильно, но не коннектится», это различие часто оказывается важным.
Базовая фабрика подключения
В приложении соединение обычно собирают в одном месте. Не надо размазывать DSN, логин, пароль и опции по контроллерам, CLI-скриптам и шаблонам.
<?php
declare(strict_types=1);
function db(): PDO
{
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
$port = $_ENV['DB_PORT'] ?? '3306';
$name = $_ENV['DB_NAME'] ?? 'app';
$user = $_ENV['DB_USER'] ?? 'app';
$pass = $_ENV['DB_PASSWORD'] ?? '';
$dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4";
return 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) относится к обычной типизации PHP из Типы и strict_types, но сам PDO всё равно возвращает данные в формах, зависящих от драйвера, SQL-типов и fetch mode. Поэтому типы доменной модели лучше приводить явно на границе: например, (int) $row['id'], если id нужен как число.
Опция PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION делает ошибки исключениями. В PHP 8 это уже поведение по умолчанию, но явная настройка полезна: код сразу показывает ожидание проекта. Дальше это раскрывается в Транзакции и режимы ошибок PDO и связано с общей моделью Ошибки, исключения и Throwable.
PDO, PDOStatement и cursor
PDO — это handle соединения. Через него выполняют SQL:
<?php
$pdo = db();
$count = $pdo->exec('DELETE FROM sessions WHERE expires_at < NOW()');exec() подходит для команд без result set: INSERT, UPDATE, DELETE, DDL. Для SELECT обычно используют query() или prepare(). query() уместен только для фиксированного SQL без внешних значений:
<?php
$stmt = $pdo->query('SELECT id, email FROM users ORDER BY id DESC LIMIT 10');
foreach ($stmt as $row) {
echo $row['email'], PHP_EOL;
}PDOStatement — это объект запроса и одновременно курсор результата. Он знает SQL, параметры, состояние выполнения и способ выдачи строк. Если в запрос попадают значения пользователя, конфигурации, $argv, $_GET или $_POST, используйте prepare():
<?php
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
if ($user === false) {
return null;
}
return [
'id' => (int) $user['id'],
'email' => $user['email'],
];Placeholders в PDO предназначены для значений, а не для имён таблиц, колонок, направлений сортировки или кусков SQL. Нельзя безопасно сделать ORDER BY :column и ожидать, что драйвер превратит это в идентификатор. Такие части SQL выбирают из whitelist: ['email', 'created_at', 'id'], а не передают напрямую из ввода. Это уже зона GET, POST и фильтрация ввода и Prepared statements и SQL injection.
Fetch modes
Fetch mode определяет форму строки результата. Самые частые варианты:
<?php
$stmt = $pdo->query('SELECT id, email FROM users LIMIT 1');
$row = $stmt->fetch(PDO::FETCH_ASSOC); // ['id' => ..., 'email' => ...]
$obj = $stmt->fetch(PDO::FETCH_OBJ); // $obj->id, $obj->email
$num = $stmt->fetch(PDO::FETCH_NUM); // [0 => ..., 1 => ...]По умолчанию исторически часто встречался PDO::FETCH_BOTH: строка индексируется и именами колонок, и числовыми индексами. В прикладном коде это редко нужно и создаёт шум. Поэтому в новых проектах обычно задают PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC.
fetch() возвращает одну следующую строку или false, если строк больше нет. fetchAll() забирает всё сразу в массив. Для маленьких справочников это нормально, но для больших выгрузок лучше итерироваться по statement, чтобы не держать весь result set в памяти. В CLI-скриптах из CLI и встроенный сервер это особенно заметно: импорт или экспорт на миллионы строк должен работать поточно.
Persistent connections
PDO::ATTR_PERSISTENT => true включает persistent connection: соединение не закрывается в конце запроса, а кешируется и может быть переиспользовано другим запросом с теми же параметрами. Это снижает цену установки соединения, но не является бесплатным ускорителем.
Главная опасность — состояние соединения. В persistent connection могут остаться временные таблицы, блокировки, session variables, незакрытые курсоры или настройки драйвера. PDO не делает полную «санитарную уборку» за приложением. Поэтому persistent connections включают только осознанно: после измерений, с понятным пулом воркеров и дисциплиной вокруг транзакций. Для обычного PHP-FPM-приложения часто разумнее начать без них.
Важно: persistent connection задаётся в массиве опций конструктора. Если вызвать setAttribute(PDO::ATTR_PERSISTENT, true) после создания объекта, драйвер уже не превратит текущее соединение в persistent.
Закрытие соединения и границы ответственности
Обычное соединение закрывается, когда объект PDO уничтожается. Явно освободить его можно так:
<?php
$stmt = null;
$pdo = null;В веб-запросе это обычно происходит автоматически в конце request lifecycle. В долгоживущих воркерах из PHP-FPM, RoadRunner и долгоживущие воркеры автоматике доверять сложнее: соединение может жить дольше одного запроса, а значит, ошибки, реконнект, транзакции и сброс состояния становятся частью архитектуры.
PDO не проверяет бизнес-правила, не валидирует HTTP-ввод, не экранирует HTML и не управляет правами доступа. Он отвечает за разговор с базой. Безопасность получается из нескольких слоёв: минимальные права DB-пользователя, корректная конфигурация из Конфигурация безопасности PHP, prepared statements, транзакции, валидация ввода и безопасный вывод.
См. также
- Prepared statements и SQL injection — как
prepare(), placeholders иexecute()защищают значения в SQL. - Транзакции и режимы ошибок PDO —
beginTransaction(),commit(),rollBack(), autocommit иPDOException. - Ошибки, исключения и Throwable — общий механизм исключений, в который попадает
PDOException. - GET, POST и фильтрация ввода — почему данные запроса нельзя напрямую превращать в SQL.
- Строки, UTF-8 и mbstring — кодировки строк и практический смысл
charset=utf8mb4. - CLI и встроенный сервер — консольные скрипты, где PDO часто используют для импортов, миграций и maintenance-задач.
- Конфигурация безопасности PHP — production-настройки, которые влияют на ошибки, секреты и поверхность атаки.