Создание, редактирование и удаление статей

Ранее в Первая запись в базе данных: форма на HTML + обработка на PHP мы рассмотрели пример как создавать статью, ниже показан код позволяющий создавать, редактировать или удалять статьи.

Файл add_form.php

<?php
require 'db_connect.php';
 
$article = null;
$isEdit = false;
 
// Если в URL есть ?id=…, загружаем статью для редактирования
if (isset($_GET['id'])) {
    $id = (int)$_GET['id'];
    $stmt = mysqli_prepare($connection, 'SELECT * FROM posts WHERE id = ?');
    mysqli_stmt_bind_param($stmt, 'i', $id);
    mysqli_stmt_execute($stmt);
    $result = mysqli_stmt_get_result($stmt);
    $article = mysqli_fetch_assoc($result);
    mysqli_stmt_close($stmt);
 
    if (!$article) {
        http_response_code(404);
        echo 'Статья не найдена';
        exit;
    }
    $isEdit = true;
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <title><?php echo $isEdit ? 'Редактировать статью' : 'Добавить статью'; ?></title>
    <link rel="stylesheet" href="site.css">
</head>
<body>
 
 
    <main class="container">
        <nav><h3><a href="./">Главная</a></h3></nav>
        <hr>
        <h2><?php echo $isEdit ? 'Редактировать статью' : 'Добавить статью'; ?></h2>
 
        <!-- action ведёт на обработчик -->
        <form method="POST" action="add_post.php">
 
 
            <?php if ($isEdit): ?>
                <input type="hidden" name="id" value="<?php echo (int)$article['id']; ?>">
            <?php endif; ?>
 
            <label for="title">Заголовок (title)</label>
            <br>
            <input type="text" name="title" id="title" required
                value="<?php echo htmlspecialchars($article['title'] ?? ''); ?>">
 
            <br>
            <label for="slug">Slug (для URL, латиница и дефисы)</label>
            <br>
            <small><span class="hint">Заполняется автоматически из заголовка (транслитерация). Можно отредактировать вручную.</span></small>
            <br>
            <input type="text" name="slug" id="slug" required
                value="<?php echo htmlspecialchars($article['slug'] ?? ''); ?>">
 
            <br>
            <label for="excerpt">Краткое описание (excerpt, опционально)</label>
            <br>
            <input type="text" name="excerpt" id="excerpt"
                value="<?php echo htmlspecialchars($article['meta_description'] ?? ''); ?>">
 
            <br>
            <label for="content">Контент (content, можно HTML)</label>
            <br>
            <textarea name="content" rows="10" cols="50" required><?php echo ($article['content'] ?? ''); ?></textarea>
 
            <br>
            <input type="checkbox" name="is_published" id="is_published" value="1" checked>
            <label for="is_published">Опубликовать сразу (is_published)</label>
 
            <br>
            <label for="sort_order">Порядок сортировки</label>
            <br>
            <input type="number" name="sort_order" id="sort_order"
                value="<?php echo (int)($article['sort_order'] ?? 0); ?>">
 
            <br><br>
            <button class="btn-main"><?php echo $isEdit ? 'Обновить статью' : 'Добавить статью'; ?></button>
 
<?php if ($isEdit): ?>
    <button class="btn-main" name="action" value="delete" class="btn-danger" onclick="return confirm('Вы уверены, что хотите удалить эту статью?');">
        Удалить статью
    </button>
<?php endif; ?>
 
 
                <!-- Блок уведомлений -->
    <?php if (isset($_GET['msg'])): ?>
        <div class="notification">
            <?php
            $msg = $_GET['msg'];
            if ($msg === 'new') {
                echo '<strong>Новая статья успешно добавлена!</strong>';
            } elseif ($msg === 'updated') {
                echo '<strong>Редактирование статьи осуществлено!</strong>';
            }
            ?>
        </div>
    <?php endif; ?>
        </form>
    </main>
 
    <script>
        const titleInput = document.getElementById('title');
        const slugInput = document.getElementById('slug');
        let isManualEdit = false;
 
        slugInput.addEventListener('input', () => {
            isManualEdit = true;
        });
 
        const translitMap = {
            'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
            'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
            'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
            'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch',
            'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'
        };
 
        function transliterate(str) {
            return str
                .toLowerCase()
                .split('')
                .map(char => translitMap[char] || char)
                .join('');
        }
 
        titleInput.addEventListener('input', () => {
            if (isManualEdit) return;
 
            const value = titleInput.value.trim();
            if (!value) {
                slugInput.value = '';
                return;
            }
 
            let slug = transliterate(value);
            slug = slug
                .replace(/[^a-z0-9]+/g, '-')
                .replace(/^-+|-+$/g, '')
                .replace(/-+/g, '-');
 
            slugInput.value = slug;
        });
    </script>
</body>
</html>

Файл add_post.php

<?php
require 'db_connect.php';
 
// Проверка соединения
if (!$connection || mysqli_connect_errno()) {
    http_response_code(500);
    echo 'Ошибка подключения к БД: ' . htmlspecialchars(mysqli_connect_error());
    exit;
}
 
// Определяем режим: если прислали id — это обновление, иначе вставка
$mode = 'insert';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
 
// Обработка удаления
if (isset($_POST['action']) && $_POST['action'] === 'delete') {
    if ($id === 0) {
        http_response_code(400);
        echo 'Ошибка: невозможно удалить статью без ID.';
        exit;
    }
 
    $sql = 'DELETE FROM posts WHERE id = ?';
    $stmt = mysqli_prepare($connection, $sql);
    mysqli_stmt_bind_param($stmt, 'i', $id);
 
    if (mysqli_stmt_execute($stmt)) {
        mysqli_stmt_close($stmt);
        header('Location: index.php');
        exit;
    } else {
        http_response_code(500);
        echo 'Ошибка при удалении: ' . htmlspecialchars(mysqli_stmt_error($stmt));
        mysqli_stmt_close($stmt);
        exit;
    }
}
 
if ($id > 0) {
    $mode = 'update';
}
 
$title          = trim($_POST['title'] ?? '');
$slug           = trim($_POST['slug'] ?? '');
$content        = $_POST['content'] ?? '';
$meta_desc      = $_POST['excerpt'] ?? null; // в форме поле называется excerpt
$meta_keys      = null;                      // если добавишь поле — раскомментируй
$is_published   = isset($_POST['is_published']) ? 1 : 0;
$sort_order     = (int)($_POST['sort_order'] ?? 0);
 
// Валидация: title, slug, content обязательны
if ($title === '' || $slug === '' || $content === '') {
    http_response_code(400);
    echo 'Ошибка: поля title, slug и content обязательны.';
    exit;
}
 
// Нормализация slug только при вставке. При редактировании оставляем как ввёл пользователь
if ($mode === 'insert') {
    $slug = preg_replace('/[^a-z0-9\-]/i', '-', $slug);
    $slug = preg_replace('/-+/', '-', $slug);
    $slug = trim($slug, '-');
    if ($slug === '') {
        $slug = 'page-' . time();
    }
}
 
if ($mode === 'insert') {
    // INSERT
    $sql = 'INSERT INTO posts (slug, title, content, meta_description, meta_keywords, is_published, sort_order)
            VALUES (?, ?, ?, ?, ?, ?, ?)';
    $stmt = mysqli_prepare($connection, $sql);
    mysqli_stmt_bind_param($stmt, 'ssssssi', $slug, $title, $content, $meta_desc, $meta_keys, $is_published, $sort_order);
} else {
    // UPDATE: обязательно передаём id в конце списка параметров
    $sql = 'UPDATE posts
            SET slug = ?, title = ?, content = ?, meta_description = ?, meta_keywords = ?, is_published = ?, sort_order = ?
            WHERE id = ?';
    $stmt = mysqli_prepare($connection, $sql);
    mysqli_stmt_bind_param($stmt, 'ssssssii', $slug, $title, $content, $meta_desc, $meta_keys, $is_published, $sort_order, $id);
}
 
if (!$stmt) {
    http_response_code(500);
    echo 'Ошибка подготовки запроса: ' . htmlspecialchars(mysqli_error($connection));
    exit;
}
 
if (!mysqli_stmt_execute($stmt)) {
    http_response_code(500);
    echo 'Ошибка БД: ' . htmlspecialchars(mysqli_stmt_error($stmt));
    mysqli_stmt_close($stmt);
    exit;
}
 
mysqli_stmt_close($stmt);
 
// Формируем сообщение и тип для редиректа
if ($mode === 'insert') {
    $new_id = mysqli_insert_id($connection);
    header('Location: add_form.php?id=' . $new_id . '&msg=new');
} else {
    header('Location: add_form.php?id=' . $id . '&msg=updated');
}
exit;
?>

Два файла:

  1. add_form.php — отображает форму, подтягивает данные статьи (если это редактирование) и показывает уведомления об успехе.
  2. add_post.php — обрабатывает отправку формы: проверяет данные, делает INSERT или UPDATE в БД, обрабатывает удаление.

Это классическая схема «view + controller» без фреймворков: логика простая, но покрывает все базовые сценарии.


Как форма понимает, это добавление или редактирование

Ключ — параметр ?id= в URL.

php
if (isset($_GET['id'])) {
    $id = (int)$_GET['id'];
    // ... запрос к БД ...
    $article = mysqli_fetch_assoc($result);
    if (!$article) {
        http_response_code(404);
        echo 'Статья не найдена';
        exit;
    }
    $isEdit = true;
}

Если id есть — загружаем статью из таблицы posts и ставим флаг $isEdit = true. Дальше форма ведёт себя по‑разному:

  • В заголовке страницы и кнопке пишем «Редактировать статью» вместо «Добавить статью».
  • Добавляем скрытое поле <input type="hidden" name="id" ...>, чтобы обработчик знал, какую запись обновлять.
  • Поля формы заполняем текущими значениями из базы.

Для вывода значений всегда используют htmlspecialchars() — это защита от XSS: если в тексте есть <script>, он не выполнится, а отобразится как текст.


Зачем нужен скрытый id при редактировании

Без него обработчик не поймёт, какую строку в таблице обновлять. В SQL для UPDATE обязательно условие WHERE id = ?. Если id не передать, либо ничего не обновится, либо (в худшем случае) обновятся все строки.


Автогенерация slug: зачем и как

Slug — это часть URL, понятная человеку и поисковику: например, tablitsy-v-html. В коде он формируется из заголовка с помощью транслитерации и замены пробелов/спецсимволов на дефисы.

Логика в JS:

js
titleInput.addEventListener('input', () => {
    if (isManualEdit) return; // если пользователь сам поправил slug — не перезаписываем
    const value = titleInput.value.trim();
    // транслитерация + очистка
    slugInput.value = slug;
});

Почему это удобно:

  • Не нужно каждый раз придумывать URL вручную.
  • Снижается риск опечаток и нечитаемых строк.
  • При этом пользователь может поправить slug вручную — для этого отслеживают факт ручного редактирования (isManualEdit).

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


Удаление статьи: как это реализовано

Удаление вынесено в отдельную ветку в обработчике:

php
if (isset($_POST['action']) && $_POST['action'] === 'delete') {
    // проверка id
    // DELETE FROM posts WHERE id = ?
    // редирект на список статей
}

Кнопка удаления в форме отправляет name="action" value="delete". Перед удалением в браузере спрашивают подтверждение через confirm() — это базовая защита от случайных кликов.

Важно: удаление делается только по id. Без id сервер сразу отдаёт ошибку 400.


Обработка формы: INSERT vs UPDATE

В add_post.php режим определяют по наличию id:

php
$mode = 'insert';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
if ($id > 0) {
    $mode = 'update';
}

Дальше — два разных SQL‑запроса:

  • INSERT — добавляет новую строку.
  • UPDATE — обновляет существующую по WHERE id = ?.

Параметры привязывают через mysqli_prepare() и mysqli_stmt_bind_param(). Это защищает от SQL‑инъекций: данные не подставляются в строку запроса, а передаются отдельно.

После успешного выполнения делают редирект (header('Location: ...')) и передают сообщение через GET‑параметр (?msg=new или ?msg=updated). Это нужно, чтобы:

  • Избежать повторной отправки формы при обновлении страницы (проблема double submit).
  • Показать пользователю уведомление об успехе на странице формы.

Валидация и обязательные поля

Перед записью в БД проверяют, что заполнены ключевые поля:

php
if ($title === '' || $slug === '' || $content === '') {
    http_response_code(400);
    echo 'Ошибка: поля title, slug и content обязательны.';
    exit;
}

Это базовая валидация на сервере. Даже если на форме есть required, серверную проверку убирать нельзя: запросы можно отправлять напрямую, минуя браузер.


Уведомления об успехе

На странице формы выводят сообщение, если в URL есть параметр msg:

php
<?php if (isset($_GET['msg'])): ?>
    <div class="notification">
        <?php
        $msg = $_GET['msg'];
        if ($msg === 'new') {
            echo '<strong>Новая статья успешно добавлена!</strong>';
        } elseif ($msg === 'updated') {
            echo '<strong>Редактирование статьи осуществлено!</strong>';
        }
        ?>
    </div>
<?php endif; ?>

Такой подход простой и понятный: пользователь сразу видит результат действия.


Важные моменты безопасности и надёжности

  • Подготовленные выражения (prepare + bind_param) — защита от SQL‑инъекций.
  • htmlspecialchars() при выводе — защита от XSS.
  • Проверка обязательных полей на сервере — защита от неполных записей.
  • Приведение типов ((int)$_GET['id']) — снижает риск ошибок и неожиданных значений.
  • Редирект после POST — защита от повторной отправки и стандартный паттерн «Post/Redirect/Get».

Чего в этой схеме пока нет (и что можно добавить позже)

  • Проверка уникальности slug: сейчас при вставке можно создать две статьи с одинаковым slug, что приведёт к проблемам в роутинге.
  • Более сложная валидация: длина полей, формат данных, проверка на дубликаты.
  • Права доступа: сейчас любой, кто имеет доступ к этим файлам, может добавлять/удалять статьи. В реальном проекте нужна авторизация и проверка прав.
  • Обработка ошибок БД более дружелюбно: вместо вывода сырых ошибок лучше логировать их и показывать пользователю нейтральное сообщение.

Как это выглядит:

 

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