Подключение к MySQL через PDO: разбор на живом примере
Как правильно открыть соединение с MySQL через PDO, настроить режим ошибок и кодировку, использовать подготовленные запросы и не подставить сайт под SQL-инъекцию.

Вы пишете на PHP, у вас есть база MySQL, и первое, что нужно сделать — открыть соединение. Способов два: старое расширение mysqli и объектный PDO. Разберём PDO на конкретной задаче: подключиться к базе интернет-магазина, вытащить товары по категории, вставить новый заказ и не оставить дыру для SQL-инъекции. Примеры проверены на PHP 8.2 и MySQL 8.0, но работают и на PHP 7.4+.
Очевидное решение — подставить переменную прямо в строку запроса — даёт две проблемы сразу: инъекцию и падение на кавычках в данных. PDO решает обе через подготовленные запросы, но только если ими действительно пользоваться, а не имитировать.
Как открыть соединение
PDO принимает DSN — строку с типом драйвера, хостом, именем базы и кодировкой. Логин и пароль идут отдельными аргументами. Четвёртый аргумент — массив опций, и именно он отличает рабочее подключение от источника молчаливых багов.
<?php
// PHP 8.2, MySQL 8.0
$host = '127.0.0.1';
$db = 'shop';
$user = 'shop_user';
$pass = 'YOUR_DB_PASSWORD';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
// Бросать исключения вместо тихого возврата false
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// Ассоциативные массивы по умолчанию
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// Настоящие подготовленные запросы на стороне сервера
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
// В проде не показывайте текст ошибки пользователю
error_log($e->getMessage());
exit('Ошибка подключения к базе данных');
}Проверка, что соединение живо: если конструктор не бросил исключение, объект $pdo готов к работе. Никакого отдельного «пинга» делать не нужно.
Зачем три опции, а не ноль
По умолчанию PDO ведёт себя неудобно, и именно на этом спотыкаются новички.
- ATTR_ERRMODE в EXCEPTION. Без этого при ошибке запроса PDO вернёт
false, а вы будете гадать, почему выборка пустая. Исключение падает сразу и по делу. - ATTR_EMULATE_PREPARES в false. По умолчанию PDO для MySQL эмулирует подготовку — подставляет значения на стороне PHP. Это работает, но с настоящими серверными prepared statements защита и типизация надёжнее.
- ATTR_DEFAULT_FETCH_MODE. Чтобы не писать
PDO::FETCH_ASSOCв каждомfetch().
Строка charset=utf8mb4 в DSN — не косметика. Без неё смайлики и часть иероглифов приедут в базу как знаки вопроса, и починить это задним числом сложнее, чем сразу указать кодировку.Выборка с параметром: без инъекций
Задача — достать товары по идентификатору категории, который пришёл из GET-параметра. Опасный способ — вклеить его в строку. Правильный — передать через плейсхолдер.
<?php
$categoryId = (int) ($_GET['category'] ?? 0);
$sql = 'SELECT id, name, price
FROM products
WHERE category_id = :cat
ORDER BY name';
$stmt = $pdo->prepare($sql);
$stmt->execute(['cat' => $categoryId]);
$products = $stmt->fetchAll();
foreach ($products as $product) {
echo $product['name'] . ' — ' . $product['price'] . "\n";
}Именованный плейсхолдер :cat и передача значения в execute() — это и есть подготовленный запрос. Значение уходит на сервер отдельно от текста SQL, поэтому кавычки, точки с запятой и DROP TABLE внутри данных остаются просто данными.
Позиционные и именованные плейсхолдеры
Вместо :cat можно писать ? и передавать массив по порядку. Разница только в читаемости.
<?php
// Позиционные плейсхолдеры
$sql = 'SELECT id, name FROM products WHERE category_id = ? AND price < ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$categoryId, 1000]);Смешивать ? и :name в одном запросе нельзя — PDO бросит исключение.
Вставка и получение нового ID
При создании заказа нужно вставить строку и сразу узнать её идентификатор для связанных записей.
<?php
$sql = 'INSERT INTO orders (user_id, total, created_at)
VALUES (:user_id, :total, NOW())';
$stmt = $pdo->prepare($sql);
$stmt->execute([
'user_id' => 42,
'total' => 3499.90,
]);
$orderId = $pdo->lastInsertId();
echo "Создан заказ №$orderId";Проверка: lastInsertId() вернёт непустое числовое значение. Метод относится к соединению, а не к запросу, поэтому вызывайте его сразу после execute(), до следующих вставок.
Несколько связанных вставок — транзакция
Заказ и его позиции должны попасть в базу вместе или никак. Для этого оберните вставки в транзакцию.
<?php
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'INSERT INTO orders (user_id, total) VALUES (:u, :t)'
);
$stmt->execute(['u' => 42, 't' => 3499.90]);
$orderId = $pdo->lastInsertId();
$itemStmt = $pdo->prepare(
'INSERT INTO order_items (order_id, product_id, qty)
VALUES (:o, :p, :q)'
);
$itemStmt->execute(['o' => $orderId, 'p' => 100, 'q' => 2]);
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
error_log($e->getMessage());
throw $e;
}Если вторая вставка упадёт, rollBack() отменит и первую. Транзакции работают только на движке InnoDB — на MyISAM они молча игнорируются.
Типичные ошибки новичков
Вот что реально ломается на первых подключениях и какие сообщения вы увидите.
SQLSTATE[HY000] [2002] Connection refused— MySQL не запущен или неверный хост/порт. Проверьте, что сервер работает и вы указали127.0.0.1, а неlocalhost(последнее иногда пытается идти через сокет).SQLSTATE[HY000] [1045] Access denied for user— неверный логин или пароль, либо у пользователя нет прав на эту базу.SQLSTATE[42S02]Base table or view not found — опечатка в имени таблицы или подключились не к той базе.Invalid parameter number— число плейсхолдеров не совпадает с числом переданных значений, или смешаны?и:name.- Выборка возвращает пусто, ошибок нет — почти всегда забыт
PDO::ATTR_ERRMODE, и настоящая ошибка запроса проглочена. Включите режим исключений и посмотрите текст.
Чего делать не надо
Не подставляйте имена таблиц и столбцов через плейсхолдеры — PDO их не поддерживает, туда идут только значения. Если имя столбца динамическое, проверяйте его по белому списку допустимых значений, а не пропускайте пользовательский ввод в SQL.
Что дальше
Соединение работает — дальше стоит вынести конфигурацию базы из кода в переменные окружения или отдельный файл вне корня сайта, чтобы пароль не попал в репозиторий. Если проект растёт, посмотрите, как PDO используется внутри Doctrine DBAL или Eloquent — обёртки берут на себя пул соединений и миграции. Отдельная тема — тайм-ауты и переподключение при разрыве долгоживущего соединения; здесь мы разобрали только базовый цикл открытия и запросов. Точный список опций и их поведение в вашей версии сверяйте с документацией PDO на php.net.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: PHP Manual: PDO — PHP Data Objects, PHP Manual: PDO::prepare
Частые вопросы
PDO или mysqli — что выбрать новичку?
А если у меня PHP 7.4, а не 8.2 — код будет работать?
Почему запрос ничего не возвращает, но и ошибки нет?
Можно ли передать имя таблицы через плейсхолдер?
Нужно ли закрывать соединение вручную?
Транзакция не откатывается — в чём дело?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.