Работа с базой данных в PHP — это подключение скрипта к СУБД (чаще всего MySQL или PostgreSQL), отправка SQL-запросов и получение результата через встроенный слой доступа. Начиная с PHP 7 рекомендуемый способ — расширение PDO (PHP Data Objects): единый интерфейс к разным СУБД плюс подготовленные выражения, которые закрывают главную угрозу — SQL-инъекции.
Содержание
- Подключение к базе данных: минимальный рабочий пример
- Выборка данных: query, fetch и fetchAll
- SQL-инъекция на конкретном коде: уязвимость, результат, исправление
- Граница защиты: что плейсхолдеры не закрывают
- Обработка ошибок через исключения PDO
- PDO или mysqli: что выбрать
- Если подключение не работает
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — рабочий пример подключения, выборка данных, подготовленные выражения и разбор инъекции на конкретном коде. В конце — таблица PDO vs mysqli и что делать, когда подключение не работает.
Актуальность на сентябрь 2026: стабильная ветка — PHP 8.5, ветка 8.4 еще на активной поддержке. Примеры совместимы с PHP 8.1 и выше.
Подключение к базе данных: минимальный рабочий пример
Соберем весь код подключения целиком — его можно скопировать и запустить, дальше разберем по частям.
<?php
$host = '127.0.0.1';
$db = 'shop';
$user = 'app_user';
$pass = getenv('DB_PASS'); // пароль из переменной окружения, не в коде
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
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);
echo "Подключение установлено\n";
} catch (PDOException $e) {
error_log('DB connect failed: ' . $e->getMessage()); // детали - в лог
exit('Сервис временно недоступен'); // пользователю - общее сообщение
}
При успехе в консоли появится Подключение установлено. Разберем ключевые места.
DSN (data source name) — строка вида mysql:host=...;dbname=...;charset=.... Драйвер в начале (mysql:) определяет СУБД; для PostgreSQL это будет pgsql:. Кодировку utf8mb4 задаем в DSN, а не отдельным запросом.
Три опции стоит выставлять всегда. ERRMODE_EXCEPTION заставляет PDO бросать исключение при любой ошибке — иначе провал запроса можно молча пропустить. FETCH_ASSOC задает выборку по умолчанию в массив с именами столбцов в ключах. EMULATE_PREPARES => false включает подготовку выражений на стороне сервера, а не эмуляцию внутри PHP.
Пароль берем из переменной окружения через getenv(), а не пишем в коде: исходники попадают в репозиторий, а секреты — нет.
Выборка данных: query, fetch и fetchAll
Для запроса без внешних параметров подойдет query(). Результат можно перебрать в цикле:
$stmt = $pdo->query('SELECT id, title, price FROM products ORDER BY id LIMIT 3');
foreach ($stmt as $row) {
echo "{$row['id']}: {$row['title']} - {$row['price']} руб.\n";
}
Ожидаемый вывод (зависит от содержимого таблицы):
1: Клавиатура - 1290 руб.
2: Мышь - 690 руб.
3: Монитор - 15990 руб.
Когда нужна одна строка, берут fetch() — он вернет массив или false, если строк нет. Для всех строк сразу есть fetchAll(), но на больших выборках он держит весь результат в памяти, поэтому для тысяч строк лучше цикл foreach.
SQL-инъекция на конкретном коде: уязвимость, результат, исправление
Главная опасность появляется, когда в запрос попадают данные от пользователя. Покажем ошибку тройкой.
Шаг 1. Уязвимый код — логин из запроса вклеен в строку SQL через конкатенацию:
$login = $_GET['login']; // пришло из формы или URL
$sql = "SELECT id, login FROM users WHERE login = '$login'";
$stmt = $pdo->query($sql);
$user = $stmt->fetch();
Шаг 2. Что происходит при вредоносном вводе. Пусть пользователь передал login со значением ' OR '1'='1. Тогда итоговый SQL становится таким:
SELECT id, login FROM users WHERE login = '' OR '1'='1'
Условие '1'='1' истинно всегда, поэтому запрос вернет первую строку таблицы users независимо от логина. Более опасные варианты ввода ('; DROP TABLE users; -- и подобные) позволяют менять и удалять данные. Это и есть SQL-инъекция: пользователь дописал логику в ваш запрос.
Шаг 3. Исправление — подготовленное выражение (prepared statement). Значение передается отдельно от текста запроса, на месте данных стоит плейсхолдер ?:
$login = $_GET['login'];
$stmt = $pdo->prepare('SELECT id, login FROM users WHERE login = ?');
$stmt->execute([$login]);
$user = $stmt->fetch();
echo $user ? "Найден: {$user['login']}\n" : "Пользователь не найден\n";
Теперь СУБД сначала получает шаблон запроса, а потом данные. Что бы ни пришло в $login, оно трактуется как одно строковое значение, а не как часть SQL. При вводе ' OR '1'='1 запрос просто не найдет такого логина и вернет Пользователь не найден.
Плейсхолдеры бывают позиционные (?) и именованные (:login). Именованные удобнее в больших запросах:
$stmt = $pdo->prepare(
'INSERT INTO products (title, price) VALUES (:title, :price)'
);
$stmt->execute([
':title' => 'Веб-камера',
':price' => 3990,
]);
echo 'Добавлена запись id=' . $pdo->lastInsertId() . "\n";
Метод lastInsertId() вернет идентификатор только что вставленной строки, если у таблицы есть автоинкрементный ключ.
Граница защиты: что плейсхолдеры не закрывают
Подготовленные выражения подставляют только значения. Имена таблиц, столбцов, ключевые слова и направление сортировки через плейсхолдер передать нельзя — такой код выдаст ошибку синтаксиса. Если имя столбца для ORDER BY приходит извне, его нельзя биндить; для этого нужен белый список допустимых значений:
$allowed = ['price', 'title', 'id']; // разрешенные столбцы
$sortBy = $_GET['sort'] ?? 'id';
$column = in_array($sortBy, $allowed, true) ? $sortBy : 'id'; // иначе значение по умолчанию
$stmt = $pdo->query("SELECT id, title FROM products ORDER BY $column");
Здесь безопасность держится не на биндинге, а на проверке значения по списку $allowed. Прямая подстановка имени столбца допустима только после такой проверки.
Обработка ошибок через исключения PDO
С опцией ERRMODE_EXCEPTION любой сбой запроса бросает PDOException. Это позволяет обернуть операции в try/catch и не проверять код возврата после каждого вызова.
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?');
$stmt->execute([100, 1]);
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?');
$stmt->execute([100, 2]);
$pdo->commit(); // обе операции применяются вместе
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack(); // откатываем, только если транзакция реально открыта
}
error_log('Transfer failed: ' . $e->getMessage());
}
Важное правило безопасности: текст исключения содержит фрагменты запроса и структуру БД, поэтому его пишут в лог, а пользователю показывают нейтральное сообщение. Показывать сырую ошибку на странице нельзя — это подсказка атакующему.
PDO или mysqli: что выбрать
Оба расширения поддерживают подготовленные выражения и безопасны при правильном использовании. Разница — в универсальности и API.
| Критерий | PDO | mysqli |
|---|---|---|
| Поддержка СУБД | много драйверов (MySQL, PostgreSQL, SQLite и другие) | только MySQL и MariaDB |
| Стиль | только объектный | объектный и процедурный |
Именованные плейсхолдеры (:name) |
есть | нет, только ? |
| Смена СУБД без переписывания запросов | да, при переносимом SQL | нет |
| Когда брать | новый проект, вероятна смена или несколько СУБД | проект уже завязан только на MySQL |
Для нового кода по умолчанию берут PDO: единый интерфейс и именованные параметры удобнее в поддержке. Расширение mysql_* (без i) удалено еще в PHP 7.0 и в современных проектах не используется — примеры с mysql_connect() из старых статей устарели.
Если подключение не работает
Диагностика по симптому, как обычно и ищут ошибку:
SQLSTATE[HY000] [1045] Access denied— неверный логин или пароль пользователя БД, либо у него нет прав на эту базу.SQLSTATE[HY000] [2002] Connection refused— сервер БД не запущен или неверный host/port в DSN.SQLSTATE[HY000] [1049] Unknown database— имя базы вdbnameне совпадает с существующей.could not find driver— не включено PDO-расширение драйвера (pdo_mysql), проверьте выводphp -m.
Отдельная защита на уровне доступов: приложение должно ходить в базу под пользователем с минимально нужными правами (например, только SELECT/INSERT/UPDATE на свои таблицы), а не под root. Тогда даже при найденной уязвимости ущерб ограничен правами этого пользователя.
Выводы
- Для новых проектов на PHP базовый выбор — PDO: единый интерфейс к разным СУБД и удобные подготовленные выражения.
- Данные от пользователя никогда не вклеивают в SQL через конкатенацию — только через плейсхолдеры
?или:name, иначе возможна SQL-инъекция. - Плейсхолдеры подставляют лишь значения; имена столбцов и таблиц проверяют по белому списку.
- Режим
ERRMODE_EXCEPTIONплюсtry/catchдают предсказуемую обработку ошибок; текст исключения пишут в лог, а не показывают пользователю. - Пароль БД держат в переменной окружения, а приложение ходит в базу под пользователем с минимальными правами.
Где применяется / связь с практикой
Работа с БД — основа почти любого backend-кода на PHP: регистрация и вход пользователей, каталоги товаров, комментарии, поиск, корзина. Именно на стыке пользовательского ввода и SQL чаще всего и появляются уязвимости, поэтому подготовленные выражения и разграничение прав — не теория, а ежедневная практика разработчика.
Освойте тему на практике
Разобрать PDO, транзакции и защиту от инъекций на реальных задачах помогает курс Разработчик PHP. Посмотреть формат занятий и темы можно на открытых уроках Otus — они бесплатные и проходят регулярно.
FAQ
Защищает ли PDO от SQL-инъекций сам по себе?
Нет. Защищают именно подготовленные выражения (prepare + execute). Если собирать запрос конкатенацией и отдавать в query(), PDO так же уязвим, как и любой другой способ.
Можно ли подставить имя таблицы или столбца через плейсхолдер?
Нет, плейсхолдеры работают только для значений. Имя столбца или таблицы проверяют по белому списку допустимых вариантов и подставляют в текст запроса вручную.
Чем query() отличается от prepare() с execute()?
query() сразу выполняет готовую строку SQL и годится только для запросов без внешних данных. Для любых значений от пользователя используют prepare() с плейсхолдерами и execute().



