Идентификаторы постов в PHP: особенности обработки при CRUD

Раздел: Веб-разработка на PHP -> Управление постами (CRUD)

Основные подходы к работе с ID постов

При разработке CRUD-системы для постов идентификатор (ID) является ключевым элементом. Наиболее эффективное решение - использование PDO с подготовленными выражениями. Это обеспечивает защиту от SQL-инъекций и явное управление типами данных.

Как безопасно передавать ID и выполнять запросы?

ID обычно приходит через GET-параметр (например, ?id=5). Его необходимо фильтровать с помощью filter_input или filter_var с флагом FILTER_VALIDATE_INT. Затем передавать в запрос через именованный плейсхолдер.

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
    // обработка ошибки
}

$stmt = $pdo->prepare('SELECT * FROM posts WHERE id = :id');
$stmt->execute([':id' => $id]);
$post = $stmt->fetch(PDO::FETCH_ASSOC);

Типичные ошибки:

  • Пропуск фильтрации – ведёт к SQL-инъекции.
  • Использование некорректных типов (строка вместо int) – может вызвать неожиданное поведение.
  • Отсутствие проверки существования записи – приводит к пустому выводу или ошибке.

Решение всех этих проблем – строгая валидация входных данных и использование PDO.

Вариант 1. Использование intval и процедурного MySQLi

Если проект использует MySQLi, ID можно преобразовать через intval или (int). Это защищает от нечисловых значений, но не от возможных ошибок приведения типов.

$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$result = mysqli_query($conn, "SELECT * FROM posts WHERE id = $id");

Как обработать случай, когда ID отсутствует или равен нулю?

После получения ID проверяем условие: если $id <= 0 – возвращаем ошибку 400 или редирект.

Недостатки:

  • Прямая подстановка ID в строку запроса – риск инъекции, если забыли привести к int.
  • MySQLi не поддерживает передачу параметров без конкатенации.

Вариант 2. Применение ctype_digit и ручное экранирование

Можно проверить, что строка состоит только из цифр: ctype_digit($_GET['id']). Затем экранировать через mysqli_real_escape_string.

if (isset($_GET['id']) && ctype_digit($_GET['id'])) {
    $id = $_GET['id'];
} else {
    $id = 0;
}
$safe_id = mysqli_real_escape_string($conn, $id);
$result = mysqli_query($conn, "SELECT * FROM posts WHERE id = '$safe_id'");

В каких случаях стоит использовать такой подход?

Только при условии, что переход на PDO невозможен (наследие). В новых проектах не рекомендуется.

Проблема:

ctype_digit не пропускает целые числа с лидирующими нулями (например, "007") – они станут числом 7, что обычно приемлемо. Однако строка "0" считается корректной, что может быть нежелательно для ID поста.

Вариант 3. Использование slug вместо числового ID

Для SEO-дружественных URL часто применяют строковый идентификатор – slug, который извлекается из заголовка или генерируется. Хранится он в колонке slug таблицы.

$slug = filter_input(INPUT_GET, 'slug', FILTER_SANITIZE_STRING);
if (empty($slug)) {
    // ошибка валидации
}
$stmt = $pdo->prepare('SELECT * FROM posts WHERE slug = :slug');
$stmt->execute([':slug' => $slug]);
$post = $stmt->fetch();
if (!$post) {
    http_response_code(404);
    exit;
}

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

Для улучшения читаемости URL, индексации поисковыми системами и упрощения навигации. Однако slug требует дополнительной обработки при создании/обновлении поста (транслитерация, уникальность).

Типичная ошибка:

Дубликат slug при изменении заголовка – необходимо генерировать уникальный ключ (например, добавлять случайные символы или ID).

Вариант 4. Получение ID из POST-данных при редактировании или удалении

При выполнении действий (обновление, удаление) ID часто передаётся скрытым полем формы. Важно проверять его так же строго, как GET-параметр, и дополнительно использовать CSRF-токены для защиты от подделки запросов.

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
    if (!$id) {
        // ошибка – невалидный ID
    }
    // далее проверка токена и выполнение запроса
}

Важно:

Не доверять полю id из формы – злоумышленник может изменить значение. Поэтому всегда перепроверяйте права доступа (например, что пост принадлежит текущему пользователю).

Расширенные примеры работы с ID постов

Пример 1: Получение поста по ID с проверкой существования и обработкой 404

В этом примере используем PDO, фильтрацию ID и проверку, что запись найдена.

Пример
// index.php?id=5
require 'db.php';

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if ($id === false || $id === null) {
    http_response_code(400);
    echo json_encode(['error' => 'Неверный ID']);
    exit;
}

try {
    $stmt = $pdo->prepare('SELECT * FROM posts WHERE id = :id LIMIT 1');
    $stmt->execute([':id' => $id]);
    $post = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$post) {
        http_response_code(404);
        echo json_encode(['error' => 'Пост не найден']);
        exit;
    }

    echo json_encode($post);
} catch (PDOException $e) {
    http_response_code(500);
    echo json_encode(['error' => 'Ошибка базы данных']);
}
# Результат при id=5 (существует):
{"id":5,"title":"Пример записи","content":"Текст поста","created_at":"2025-03-31"}

# Результат при id=999 (не существует):
{"error":"Пост не найден"}

# Результат при id=abc:
{"error":"Неверный ID"}

Пример 2: Обновление поста с проверкой ID через PDO

Форма передаёт POST-запрос с полями: post_id, title, content.

Пример
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
    $title   = filter_input(INPUT_POST, 'title', FILTER_SANITIZE_STRING);
    $content = filter_input(INPUT_POST, 'content', FILTER_SANITIZE_STRING);

    if (!$post_id || !$title || !$content) {
        // сообщение об ошибке
        exit;
    }

    $stmt = $pdo->prepare('UPDATE posts SET title = :title, content = :content WHERE id = :id');
    $stmt->execute([
        ':title'   => $title,
        ':content' => $content,
        ':id'      => $post_id
    ]);

    if ($stmt->rowCount() === 0) {
        // ID не найден или данные не изменились
        echo 'Пост с указанным ID не найден';
    } else {
        echo 'Пост успешно обновлён';
    }
}
# При успешном обновлении:
Пост успешно обновлён

# Если ID не существует (rowCount = 0):
Пост с указанным ID не найден

# При пропущенном поле title:
// Появится сообщение об ошибке валидации (зависит от реализации)

Пример 3: Удаление поста с защитой от случайного подтверждения

Используется HTTP-метод DELETE (имитация через POST с параметром _method) или POST с полем confirm.

Пример
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['_method']) && $_POST['_method'] === 'DELETE') {
    $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
    $confirm = isset($_POST['confirm']) && $_POST['confirm'] === 'yes';

    if (!$id || !$confirm) {
        http_response_code(400);
        exit('Подтверждение удаления обязательно');
    }

    $stmt = $pdo->prepare('DELETE FROM posts WHERE id = :id');
    $stmt->execute([':id' => $id]);

    if ($stmt->rowCount()) {
        echo 'Пост удалён';
    } else {
        http_response_code(404);
        echo 'Пост не найден';
    }
}
# Пример успешного удаления:
Пост удалён

# Если checkbox подтверждения не отмечен:
Подтверждение удаления обязательно

Пример 4: Маршрутизация на основе ID с обработкой нескольких действий

Единый скрипт post.php обрабатывает GET, POST, PUT, DELETE через параметр action и id.

Пример
$action = $_GET['action'] ?? 'view';
$id     = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if (!$id && in_array($action, ['show', 'edit', 'delete'])) {
    http_response_code(400);
    exit('Требуется ID');
}

switch ($action) {
    case 'show':
        $stmt = $pdo->prepare('SELECT * FROM posts WHERE id = :id');
        $stmt->execute([':id' => $id]);
        $post = $stmt->fetch();
        if (!$post) {
            http_response_code(404);
            exit('Пост не найден');
        }
        echo json_encode($post);
        break;

    case 'edit':
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            // аналогично обновлению из Примера 2
        }
        break;

    case 'delete':
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            // удаление
        }
        break;

    default:
        // список всех постов
}
# Запрос: post.php?action=show&id=10
{"id":10,"title":"Десятый пост","content":"..."}

# Запрос post.php?action=show (без id)
Требуется ID

Пример 5: Использование UUID вместо автоинкрементного ID

В некоторых системах применяют UUID (32 символа + дефисы). Тогда ID хранится как строка.

Пример
$uuid = filter_input(INPUT_GET, 'uuid', FILTER_VALIDATE_REGEXP, [
    'options' => ['regexp' => '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i']
]);

if (!$uuid) {
    http_response_code(400);
    exit('Неверный формат UUID');
}

$stmt = $pdo->prepare('SELECT * FROM posts WHERE uuid = :uuid');
$stmt->execute([':uuid' => $uuid]);
$post = $stmt->fetch();
# Корректный запрос: /post.php?uuid=550e8400-e29b-41d4-a716-446655440000
# Ответ – данные поста.

# Некорректный UUID:
Неверный формат UUID

Посты с ID в PHP - comments

En
Posts php id (php)