Ранее в Первая запись в базе данных: форма на 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; ?>
Два файла:
add_form.php— отображает форму, подтягивает данные статьи (если это редактирование) и показывает уведомления об успехе.add_post.php— обрабатывает отправку формы: проверяет данные, делает INSERT или UPDATE в БД, обрабатывает удаление.
Это классическая схема «view + controller» без фреймворков: логика простая, но покрывает все базовые сценарии.
Как форма понимает, это добавление или редактирование
Ключ — параметр ?id= в URL.
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:
titleInput.addEventListener('input', () => { if (isManualEdit) return; // если пользователь сам поправил slug — не перезаписываем const value = titleInput.value.trim(); // транслитерация + очистка slugInput.value = slug; });
Почему это удобно:
- Не нужно каждый раз придумывать URL вручную.
- Снижается риск опечаток и нечитаемых строк.
- При этом пользователь может поправить slug вручную — для этого отслеживают факт ручного редактирования (
isManualEdit).
На стороне сервера slug дополнительно нормализуют: убирают лишние дефисы, приводят к нижнему регистру. При вставке (INSERT) ещё и генерируют запасной вариант, если slug получился пустым.
Удаление статьи: как это реализовано
Удаление вынесено в отдельную ветку в обработчике:
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:
$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).
- Показать пользователю уведомление об успехе на странице формы.
Валидация и обязательные поля
Перед записью в БД проверяют, что заполнены ключевые поля:
if ($title === '' || $slug === '' || $content === '') { http_response_code(400); echo 'Ошибка: поля title, slug и content обязательны.'; exit; }
Это базовая валидация на сервере. Даже если на форме есть required, серверную проверку убирать нельзя: запросы можно отправлять напрямую, минуя браузер.
Уведомления об успехе
На странице формы выводят сообщение, если в URL есть параметр msg:
<?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, что приведёт к проблемам в роутинге. - Более сложная валидация: длина полей, формат данных, проверка на дубликаты.
- Права доступа: сейчас любой, кто имеет доступ к этим файлам, может добавлять/удалять статьи. В реальном проекте нужна авторизация и проверка прав.
- Обработка ошибок БД более дружелюбно: вместо вывода сырых ошибок лучше логировать их и показывать пользователю нейтральное сообщение.
Как это выглядит:

