Menu

PHP PDO: подключение к MySQL и подготовленные запросы

PDO это слой PHP для работы с базами данных: подключитесь через new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass), а затем выполняйте каждый запрос со значениями через prepare() и execute([$value]). Подключение, подготовленные запросы, получение строк, вставка, транзакции и WHERE IN.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

PDO (PHP Data Objects) это встроенный способ работать с базой данных. Подключитесь через new PDO($dsn, $user, $password), а затем выполняйте любой запрос, где есть значение, через prepare() и execute(), чтобы значение отправлялось отдельно от SQL:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();      // an array, or false if no row matched

Примерам на этой странице нужен сервер базы данных, поэтому они показаны обычным кодом. Пример с SQL-инъекцией ниже запускается.

Подключение к MySQL через PDO

Первый аргумент это DSN: имя драйвера, а за ним параметры подключения. Держите подключение в одном месте и подключайте его там, где оно нужно.

<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // the default since PHP 8.0
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // rows as ['id' => 1, ...]
    PDO::ATTR_EMULATE_PREPARES   => false,                   // real prepared statements, real int types
];

try {
    $pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
    error_log($e->getMessage());
    http_response_code(500);
    exit('Database unavailable.');
}

return $pdo;
  • charset=utf8mb4 должен быть в DSN. Без него кириллица, диакритика и эмодзи могут приходить как ? или испорченные байты.
  • Никогда не выводите $e->getMessage() посетителям: там могут быть хост и имя пользователя. Пишите его в лог.
  • Держите пароль вне кода, в переменной окружения или файле настроек за пределами корня сайта.

Для других баз данных меняется только DSN: pgsql:host=localhost;dbname=shop для PostgreSQL, sqlite:/var/data/app.db для файла SQLite. Для каждой нужен установленный драйвер PDO (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m показывает, какие у вас есть.

Подготовленные запросы с ? и :name

Плейсхолдер отмечает, куда идёт значение. Используйте позиционные плейсхолдеры ? со списком или именованные с ассоциативным массивом:

<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);

// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);

// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();

Не ставьте кавычки вокруг плейсхолдера: WHERE name = '?' сравнивает с буквальным текстом ?. Подготовленный запрос можно выполнить много раз с разными значениями, поэтому готовьте его один раз до цикла, а не внутри него.

Почему склеивать SQL из строк опасно

Это единственный блок на странице, который запускается: он строит запрос неправильным и правильным способом для одного и того же ввода. Измените $email и запустите снова.

В склеенном запросе кавычка из ввода закрывает строку, а OR '1'='1' делает условие истинным для каждой строки: запрос возвращает всех пользователей. На форме входа это значит войти, не зная пароля, а если драйвер принимает несколько инструкций за один вызов, ввод вроде '; DROP TABLE users; -- выполняет вторую инструкцию по выбору атакующего. В подготовленном варианте база данных получает SQL и значение отдельно, поэтому кавычка это просто символ внутри email, который ни с кем не совпадает.

Функции экранирования вроде addslashes() не решение: они пропускают случаи, которые зависят от кодировки соединения. Используйте плейсхолдеры для каждого значения, включая числа и значения, которые "приходят из вашего собственного кода".

Получение строк: fetch, fetchAll и fetchColumn

<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch();                 // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
    http_response_code(404);
}

// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();

// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
    echo $row['name'], "\n";
}

// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();

// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR);  // [42 => 'Ada', ...]

query() подходит, когда в SQL нет значений извне. Как только появляется переменная, переходите на prepare().

Вставка, обновление и удаление

<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();

$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount();                 // rows changed

$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();

lastInsertId() возвращает строку, поэтому приводите её к int. В MySQL rowCount() после UPDATE считает строки, которые действительно изменились, поэтому присвоение имени того же значения, что уже было, даёт 0. Храните пароли через password_hash, никогда открытым текстом.

Транзакции

Транзакция заставляет несколько инструкций выполниться или не выполниться вместе. Если что-то выбрасывает исключение, сделайте откат, и ничего не сохранится:

<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
    $pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);

    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    throw $e;
}

Это опирается на то, что ошибки выбрасывают исключения (по умолчанию начиная с PHP 8.0), поэтому неудачная инструкция переходит в catch. В MySQL таблицы должны использовать InnoDB; MyISAM игнорирует транзакции. Throwable и повторный выброс описаны на странице про try/catch.

WHERE IN с массивом

Один ? вмещает одно значение, поэтому IN (?) с массивом не работает. Соберите по плейсхолдеру на значение:

<?php
$ids = [3, 8, 15];

if ($ids) {
    $placeholders = implode(',', array_fill(0, count($ids), '?'));   // "?,?,?"
    $stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
    $stmt->execute($ids);
    $products = $stmt->fetchAll();
} else {
    $products = [];                     // IN () would be a syntax error
}

Строка, вставленная в SQL, состоит только из вопросительных знаков и запятых, поэтому запрос остаётся безопасным.

Частые ошибки с плейсхолдерами

  • Имена таблиц и столбцов. ORDER BY ? сортирует по константной строке, а не по столбцу. Выбирайте идентификаторы из списка разрешённых: $sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, затем вставляйте $sort в SQL.
  • LIMIT ? с эмуляцией подготовки. С включённым по умолчанию в MySQL ATTR_EMULATE_PREPARES execute(['10']) отправляет LIMIT '10', а это синтаксическая ошибка. Привяжите значение с PDO::PARAM_INT или выключите эмуляцию.
  • Один и тот же именованный плейсхолдер дважды. WHERE first = :q OR last = :q работает только с эмуляцией подготовки. С настоящей подготовкой вы получите SQLSTATE[HY093]: Invalid parameter number; используйте :q1 и :q2.
  • Забыть, что fetch() возвращает false. $user['name'] для false даёт предупреждение и null; сначала проверяйте результат.

Часто задаваемые вопросы

Как подключиться к MySQL через PDO?

Создайте объект PDO с DSN, пользователем и паролем: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Неудачное подключение выбрасывает PDOException; перехватите его, запишите сообщение в лог и покажите посетителю общую ошибку.

Защищают ли подготовленные запросы от SQL-инъекций?

Да, для значений: с prepare('... WHERE id = ?') и execute([$id]) значение отправляется как данные и никогда не может изменить запрос. Плейсхолдеры не могут заменять имена таблиц, столбцов или ASC/DESC, поэтому проверяйте их по списку разрешённых имён, прежде чем вставлять в SQL.

Чем fetch отличается от fetchAll в PDO?

fetch() возвращает следующую строку (или false, когда строк больше нет), поэтому подходит для одной строки или цикла по большому результату. fetchAll() возвращает все строки массивом сразу. fetchColumn() возвращает одно значение из следующей строки, что удобно для SELECT COUNT(*).

Как использовать WHERE IN с массивом в PDO?

Соберите по одному ? на значение и передайте массив в execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Сначала проверьте, что массив не пуст, потому что IN () это синтаксическая ошибка.

Что использовать: PDO или mysqli?

Используйте PDO, если не нужна возможность, которая есть только в MySQL. Его API одинаков для MySQL, PostgreSQL, SQLite и других баз, он поддерживает именованные плейсхолдеры, а начиная с PHP 8.0 по умолчанию выбрасывает исключения при ошибках. mysqli работает только с MySQL и MariaDB.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ