W3docs

Подготовленные выражения PHP MySQL: исчерпывающее руководство

Подготовленные выражения — мощный инструмент для повышения безопасности и эффективности PHP-приложений, работающих с базами данных.

Подготовленные выражения — это важнейший метод написания безопасного PHP-кода, работающего с базой данных. Они отделяют SQL-команду от данных, которыми она оперирует, что полностью предотвращает SQL-инъекции и ускоряет выполнение многократно повторяющихся запросов. В этом руководстве объясняется, что такое подготовленные выражения, почему они важны и как использовать их с расширениями MySQLi и PDO.

На этой странице рассматривается:

  • Что такое подготовленное выражение и почему существует модель «скомпилировать один раз, выполнять много раз»
  • Написание подготовленных запросов INSERT и SELECT с помощью MySQLi
  • Те же шаблоны с PDO (именованные плейсхолдеры)
  • Распространённые ошибки: отчёты об ошибках, привязка неверного типа и повторное использование выражений

Что такое подготовленные выражения?

Подготовленное выражение — это SQL-запрос, отправляемый в базу данных в два этапа:

  1. Подготовка — вы отправляете SQL с плейсхолдерами ? (или :name) вместо реальных значений. База данных единожды разбирает, компилирует и оптимизирует этот шаблон.
  2. Выполнение — вы отправляете фактические значения отдельно. База данных подставляет их в уже скомпилированный план и запускает его.

Поскольку значения передаются по отдельному каналу от текста SQL, база данных никогда не путает данные с командами. Значение вроде ' OR '1'='1 воспринимается как обычная строка для поиска, а не как SQL для выполнения — именно поэтому атаки инъекциями не работают против подготовленных выражений.

Зачем использовать подготовленные выражения?

  • Безопасность. Пользовательский ввод никогда не может изменить структуру запроса. Это рекомендованная защита от SQL-инъекций, и именно поэтому никогда не следует формировать запросы конкатенацией переменных в строку.
  • Производительность. Запрос разбирается и компилируется один раз. Если вы выполняете его многократно (например, вставляете 1 000 строк в цикле), база данных повторно использует один и тот же план, не разбирая его каждый раз заново.
  • Более чистый код. Плейсхолдеры избавляют от ручного экранирования с помощью mysqli_real_escape_string() и необходимости следить за кавычками. Вы привязываете переменную — и всё готово.

Практическое правило: как только любая часть запроса поступает из пользовательского ввода — поле формы, параметр URL, cookie — используйте подготовленное выражение.

Шаги

Каждое подготовленное выражение следует одному жизненному циклу:

  1. Подключитесь к базе данных.
  2. Подготовьте SQL с плейсхолдерами.
  3. Привяжите переменные к плейсхолдерам.
  4. Выполните выражение.
  5. Получите результаты (для запросов SELECT).
  6. Закройте выражение.

Подготовленный 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().

Практика

Практика
Какова цель использования подготовленных выражений MySQL?
Какова цель использования подготовленных выражений MySQL?
Was this page helpful?