Идентификаторы постов в 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