Подготовленные выражения PHP MySQL: исчерпывающее руководство
Подготовленные выражения — мощный инструмент для повышения безопасности и эффективности PHP-приложений, работающих с базами данных.
Подготовленные выражения — это важнейший метод написания безопасного PHP-кода, работающего с базой данных. Они отделяют SQL-команду от данных, которыми она оперирует, что полностью предотвращает SQL-инъекции и ускоряет выполнение многократно повторяющихся запросов. В этом руководстве объясняется, что такое подготовленные выражения, почему они важны и как использовать их с расширениями MySQLi и PDO.
На этой странице рассматривается:
- Что такое подготовленное выражение и почему существует модель «скомпилировать один раз, выполнять много раз»
- Написание подготовленных запросов
INSERTиSELECTс помощью MySQLi - Те же шаблоны с PDO (именованные плейсхолдеры)
- Распространённые ошибки: отчёты об ошибках, привязка неверного типа и повторное использование выражений
Что такое подготовленные выражения?
Подготовленное выражение — это SQL-запрос, отправляемый в базу данных в два этапа:
- Подготовка — вы отправляете SQL с плейсхолдерами
?(или:name) вместо реальных значений. База данных единожды разбирает, компилирует и оптимизирует этот шаблон. - Выполнение — вы отправляете фактические значения отдельно. База данных подставляет их в уже скомпилированный план и запускает его.
Поскольку значения передаются по отдельному каналу от текста SQL, база данных никогда не путает данные с командами. Значение вроде ' OR '1'='1 воспринимается как обычная строка для поиска, а не как SQL для выполнения — именно поэтому атаки инъекциями не работают против подготовленных выражений.
Зачем использовать подготовленные выражения?
- Безопасность. Пользовательский ввод никогда не может изменить структуру запроса. Это рекомендованная защита от SQL-инъекций, и именно поэтому никогда не следует формировать запросы конкатенацией переменных в строку.
- Производительность. Запрос разбирается и компилируется один раз. Если вы выполняете его многократно (например, вставляете 1 000 строк в цикле), база данных повторно использует один и тот же план, не разбирая его каждый раз заново.
- Более чистый код. Плейсхолдеры избавляют от ручного экранирования с помощью
mysqli_real_escape_string()и необходимости следить за кавычками. Вы привязываете переменную — и всё готово.
Практическое правило: как только любая часть запроса поступает из пользовательского ввода — поле формы, параметр URL, cookie — используйте подготовленное выражение.
Шаги
Каждое подготовленное выражение следует одному жизненному циклу:
- Подключитесь к базе данных.
- Подготовьте SQL с плейсхолдерами.
- Привяжите переменные к плейсхолдерам.
- Выполните выражение.
- Получите результаты (для запросов
SELECT). - Закройте выражение.
Подготовленный INSERT с MySQLi
MySQLi использует позиционные плейсхолдеры ?. Их привязка выполняется через mysqli_stmt_bind_param(), где первый аргумент — строка типов: s для string, i для integer, d для double/float, b для blob.
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // throw on errors
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "INSERT INTO users (name, email) VALUES (?, ?)");
// "ss" => both placeholders are strings, in order
mysqli_stmt_bind_param($stmt, "ss", $name, $email);
$name = "John";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // inserts John
$name = "Jane";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // reuses the same compiled statement, inserts Jane
mysqli_stmt_close($stmt);
mysqli_close($conn);bind_param привязывает по ссылке, поэтому вы можете изменить $name/$email и снова вызвать execute() без повторной привязки — новые значения подхватятся автоматически. Это и есть преимущество модели «скомпилировать один раз, выполнять много раз» в действии.
Подготовленный SELECT с MySQLi
Для SELECT вы выполняете выражение, а затем считываете строки. Самый удобный способ — mysqli_stmt_get_result(), который возвращает обычный результирующий набор, по которому можно итерироваться:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "SELECT id, name FROM users WHERE email = ?");
mysqli_stmt_bind_param($stmt, "s", $email);
$email = "[email protected]";
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
echo $row["id"] . ": " . $row["name"] . "\n";
}
mysqli_stmt_close($stmt);
mysqli_close($conn);Подготовленные выражения с PDO
PDO — другое распространённое расширение для работы с базами данных. Многие разработчики предпочитают его, поскольку оно работает с различными СУБД и поддерживает именованные плейсхолдеры (:email), которые легче читать.
<?php
$pdo = new PDO(
"mysql:host=localhost;dbname=database;charset=utf8mb4",
"username",
"password",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// INSERT with named placeholders
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
$stmt->execute([":name" => "John", ":email" => "[email protected]"]);
// SELECT and fetch
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE email = :email");
$stmt->execute([":email" => "[email protected]"]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo $row["id"] . ": " . $row["name"] . "\n";
}Обратите внимание: с PDO вам не нужно объявлять типы — вы передаёте ассоциативный массив значений непосредственно в execute(). Установка PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION заставляет PDO выбрасывать исключение при сбое, так что проблемы никогда не остаются незамеченными.
Распространённые ошибки
- Забытый отчёт об ошибках. По умолчанию MySQLi может завершаться с ошибкой молча. Вызывайте
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)(или используйте исключения PDO), чтобы неверный запрос выбрасывал исключение, а не возвращалfalse. - Неверное количество символов в строке типов. Строка типов в
bind_paramдолжна содержать ровно по одному символу на каждый?."ss"для двух плейсхолдеров,"si"для строки, а затем целого числа. - Плейсхолдер там, где SQL его не допускает. Вы можете привязывать значения, но не идентификаторы.
WHERE id = ?работает;ORDER BY ?илиSELECT * FROM ?— нет: имена таблиц и столбцов должны быть жёстко закодированы или находиться в белом списке. - Конкатенация «всего одного» значения. Безопасных исключений не существует. Если оно пришло от пользователя — привязывайте его.
Заключение
Подготовленные выражения разделяют запрос на скомпилированный SQL-шаблон и значения, которые его заполняют. Это разделение делает их одновременно безопасными (защита от инъекций) и быстрыми (разбираются один раз, выполняются многократно). Используйте плейсхолдеры ? MySQLi или именованные плейсхолдеры :value PDO, но всегда привязывайте пользовательский ввод вместо того, чтобы формировать SQL вручную.
Продолжите изучение смежных тем: Подключение к MySQL, Вставка данных, Выборка данных и справочник по mysqli_prepare().