Что такое 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 --> B
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 --> B
PDO даёт общий PHP-интерфейс, но реальное соединение выполняет конкретный драйвер базы данных.
Быстрое повторение
Почему PDO не избавляет от знания конкретной СУБД, даже если даёт единый PHP-интерфейс?

DSN и драйверы

Соединение создаётся через конструктор:

<?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 `mysql:host=127.0.0.1;port=3306;dbname=app;charset=utf8mb4` какую роль играет часть до первого двоеточия?

Базовая фабрика подключения

В приложении соединение обычно собирают в одном месте. Не надо размазывать 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` в одной функции или фабрике, а не писать DSN и опции в каждом месте?

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, транзакции, валидация ввода и безопасный вывод.

См. также

Источники

  1. PHP Manual: PHP Data Objects
  2. PHP Manual: PDO::__construct
  3. PHP Manual: PDO Drivers
  4. PHP Manual: PDO::setAttribute
  5. PHP Manual: PDOStatement::fetch
  6. PHP Manual: Connections and Connection management
  7. PHP Manual: Errors and error handling
  8. PHP Manual: PDO_MYSQL DSN
  9. PHP Manual: PDO_SQLITE DSN