PDO в PHP: как безопасно работать с базой данных

PDO — это инструмент в PHP, чтобы общаться с базой данных. Он умеет работать с разными базами (MySQL, PostgreSQL и другими) и помогает делать это безопасно — чтобы злоумышленники не могли испортить данные через запросы.


Как подключиться к базе

Создаём объект PDO и передаём туда адрес базы, имя, пароль и настройки.

php
$dsn = 'mysql:host=localhost;dbname=test_db;charset=utf8mb4';
$user = 'db_user';
$pass = 'db_password';
 
try {
    $pdo = new PDO($dsn, $user, $pass, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ]);
} catch (PDOException $e) {
    error_log('Ошибка подключения: ' . $e->getMessage());
    http_response_code(500);
    echo json_encode(['error' => 'Ошибка сервера']);
    exit;
}

Что значат настройки — простыми словами:

  • ERRMODE_EXCEPTION: если случится ошибка, PHP сразу скажет об этом через исключение. Так проще ловить ошибки в коде.
  • FETCH_ASSOC: когда получаем данные из базы, они приходят как обычный массив с названиями полей: ['id' => 1, 'name' => 'Ivan'].
  • EMULATE_PREPARES => false: база сама будет правильно обрабатывать запросы. Это надёжнее.

Зачем нужны подготовленные выражения

Это способ делать запросы так, чтобы нельзя было «вставить» вредоносный код. Работает так: сначала готовим шаблон запроса, потом отдельно передаём данные. База сама всё правильно подставит.

Вариант с ? (по порядку)

php
$sql = 'SELECT * FROM users WHERE email = ? AND active = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST['email'], 1]);
$user = $stmt->fetch();

Здесь ? — это пустые места. Данные подставляются по порядку.

Вариант с :имя (по названиям)

Удобнее, если параметров много:

php
$sql = 'SELECT * FROM users WHERE email = :email AND active = :active';
$stmt = $pdo->prepare($sql);
$stmt->execute([
    'email'  => $_POST['email'],
    'active' => 1,
]);
$user = $stmt->fetch();

Можно писать ключи без двоеточия — PDO сам поймёт.


Примеры простых запросов

Получить одну строку (SELECT)

php
$sql = 'SELECT id, name, email FROM users WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $_GET['id']]);
$row = $stmt->fetch();
 
if ($row === false) {
    http_response_code(404);
    echo json_encode(['error' => 'Пользователь не найден']);
    exit;
}

Получить несколько строк

php
$limit = (int)$_GET['limit'] ?? 10;
$limit = max(1, min($limit, 100)); // чтобы не прислали слишком большое число
 
$sql = "SELECT id, name FROM users WHERE active = :active LIMIT $limit";
$stmt = $pdo->prepare($sql);
$stmt->execute(['active' => 1]);
$rows = $stmt->fetchAll();

Важно: LIMIT нельзя передавать как параметр в таком режиме. Поэтому берём число, проверяем и вставляем прямо в запрос.

Вставить запись (INSERT)

php
$sql = 'INSERT INTO users (email, name, created_at) VALUES (:email, :name, :created_at)';
$stmt = $pdo->prepare($sql);
 
$stmt->execute([
    'email'      => $_POST['email'],
    'name'       => $_POST['name'],
    'created_at' => date('Y-m-d H:i:s'),
]);
 
$lastId = $pdo->lastInsertId(); // ID новой строки

Обновить запись (UPDATE)

php
$sql = 'UPDATE users SET name = :name, email = :email WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute([
    'name'  => $_POST['name'],
    'email' => $_POST['email'],
    'id'    => $_POST['id'],
]);
 
$affected = $stmt->rowCount(); // сколько строк обновилось
if ($affected === 0) {
    // Либо ID не существует, либо данные не изменились
}

Удалить запись (DELETE)

php
$sql = 'DELETE FROM users WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $_POST['id']]);
 
$deleted = $stmt->rowCount();
if ($deleted === 0) {
    // Ничего не удалилось: такого ID нет или уже удалён
}

Самое главное правило безопасности

Никогда не собирайте запрос вручную из пользовательских данных:

php
// Так нельзя — это опасно!
$sql = "SELECT * FROM users WHERE email = '{$_POST['email']}'";

Если пользователь введёт хитрую строчку, он сможет сломать базу. Всегда используйте prepare и execute.


Транзакции: когда нужно сделать сразу несколько действий

Бывает, что надо выполнить два действия вместе: если одно не получилось — отменить и второе. Например, списать деньги и записать историю.

php
// Так нельзя — это опасно!
$sql = "SELECT * FROM users WHERE email = '{$_POST['email']}'";
try {
    $pdo->beginTransaction();
 
    $stmt = $pdo->prepare('UPDATE accounts SET balance = balance - :amount WHERE id = :id');
    $stmt->execute(['amount' => $amount, 'id' => $userId]);
 
    $stmt = $pdo->prepare('INSERT INTO history (user_id, amount, type) VALUES (:id, :amount, :type)');
    $stmt->execute(['id' => $userId, 'amount' => $amount, 'type' => 'withdraw']);
 
    $pdo->commit(); // подтверждаем оба действия
} catch (PDOException $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack(); // отменяем всё, если что-то пошло не так
    }
    error_log('Ошибка транзакции: ' . $e->getMessage());
    http_response_code(500);
}

Как понимать, что пошло не так

С настройкой ERRMODE_EXCEPTION любая ошибка сразу превращается в исключение. Ловим его через try/catch:

php
try {
    $stmt = $pdo->prepare('SELECT * FROM non_existent_table');
    $stmt->execute();
} catch (PDOException $e) {
    error_log('Ошибка БД: ' . $e->getMessage()); // пишем в лог
    http_response_code(500);
    echo json_encode(['error' => 'Произошла ошибка']); // пользователю — без деталей
}

Пользователю не надо показывать текст ошибки — там может быть лишняя информация про базу.


Частые ошибки, которых лучше избегать

  • Включать эмуляцию подготовленных выражений (EMULATE_PREPARES = true). Лучше оставить false.
  • Передавать через параметры имена таблиц, колонок, LIMIT. Это не работает как надо. Проверяйте такие значения и вставляйте в запрос явно.
  • Не смотреть на rowCount() в UPDATE и DELETE. Полезно знать, сколько строк реально затронуто.
  • Делать несколько связанных действий без транзакции. Если посередине случится ошибка, данные могут стать несогласованными.

Простой пример функций для пользователей

php
function getUserById(PDO $pdo, int $id): ?array
{
    $sql = 'SELECT id, name, email, active FROM users WHERE id = :id';
    $stmt = $pdo->prepare($sql);
    $stmt->execute(['id' => $id]);
    return $stmt->fetch() ?: null;
}
 
function createUser(PDO $pdo, string $name, string $email): int
{
    $sql = 'INSERT INTO users (name, email, created_at) VALUES (:name, :email, :created_at)';
    $stmt = $pdo->prepare($sql);
    $stmt->execute([
        'name'       => $name,
        'email'      => $email,
        'created_at' => date('Y-m-d H:i:s'),
    ]);
    return (int)$pdo->lastInsertId();
}
 
function updateUser(PDO $pdo, int $id, string $name, string $email): bool
{
    $sql = 'UPDATE users SET name = :name, email = :email WHERE id = :id';
    $stmt = $pdo->prepare($sql);
    $stmt->execute([
        'name'  => $name,
        'email' => $email,
        'id'    => $id,
    ]);
    return $stmt->rowCount() > 0;
}
 
function deleteUser(PDO $pdo, int $id): bool
{
    $sql = 'DELETE FROM users WHERE id = :id';
    $stmt = $pdo->prepare($sql);
    $stmt->execute(['id' => $id]);
    return $stmt->rowCount() > 0;
}

Добавить комментарий