Разработка функционала создания темы в PHP: от простого к защищенному

Раздел: Разработка на PHP -> Разработка форумов на PHP

Создание темы на форуме: подходы и реализация

Как создать тему на форуме с обработкой формы, безопасностью и валидацией данных?

Наиболее эффективное решение основано на использовании PDO с подготовленными запросами, валидацией входных данных, защитой от CSRF‑атак и транзакционностью при добавлении темы и первого сообщения. Ниже приведён полный пример.


<?php
// config.php
session_start();
$dsn = 'mysql:host=localhost;dbname=forum;charset=utf8mb4';
$username = 'root';
$password = '';
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];
$pdo = new PDO($dsn, $username, $password, $options);

// create_topic.php
require 'config.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // CSRF проверка
    if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
        die('Ошибка CSRF.');
    }

    $title = trim($_POST['title'] ?? '');
    $content = trim($_POST['content'] ?? '');
    $user_id = $_SESSION['user_id'] ?? 0;
    $category_id = (int)($_POST['category_id'] ?? 0);

    // Валидация
    $errors = [];
    if (mb_strlen($title) < 3 || mb_strlen($title) > 255) {
        $errors[] = 'Заголовок должен быть от 3 до 255 символов.';
    }
    if (empty($content)) {
        $errors[] = 'Содержание не может быть пустым.';
    }
    if ($category_id <= 0) {
        $errors[] = 'Выберите категорию.';
    }
    if ($user_id === 0) {
        $errors[] = 'Пользователь не авторизован.';
    }

    if (empty($errors)) {
        try {
            $pdo->beginTransaction();

            // Вставка темы
            $stmt = $pdo->prepare('INSERT INTO topics (title, user_id, category_id, created_at) VALUES (?, ?, ?, NOW())');
            $stmt->execute([$title, $user_id, $category_id]);
            $topic_id = $pdo->lastInsertId();

            // Вставка первого сообщения
            $stmt = $pdo->prepare('INSERT INTO posts (topic_id, user_id, content, created_at) VALUES (?, ?, ?, NOW())');
            $stmt->execute([$topic_id, $user_id, $content]);

            $pdo->commit();
            $_SESSION['success'] = 'Тема успешно создана.';
            header('Location: viewtopic.php?t=' . $topic_id);
            exit;
        } catch (PDOException $e) {
            $pdo->rollBack();
            $errors[] = 'Ошибка базы данных: ' . $e->getMessage();
        }
    }
}

// Генерация CSRF токена
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
?>
<!DOCTYPE html>
<html>
<body>
<form method="post" action="create_topic.php">
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
    Заголовок: <input type="text" name="title"><br>
    Содержание: <textarea name="content"></textarea><br>
    Категория: <select name="category_id">
        <?php
        $cats = $pdo->query('SELECT id, name FROM categories')->fetchAll();
        foreach ($cats as $cat) {
            echo '<option value="' . $cat['id'] . '"' . ($_POST['category_id'] ?? '') == $cat['id'] ? ' selected' : '' . '>' . htmlspecialchars($cat['name']) . '</option>';
        }
        ?>
    </select><br>
    <button type="submit">Создать тему</button>
</form>
<?php if (!empty($errors)): ?>
    <div class="errors">
        <?= implode('<br>', array_map('htmlspecialchars', $errors)) ?>
    </div>
<?php endif; ?>
</body>
</html>
  

Viewtopic php t create (создание темы на форуме в php)

Пояснение шагов:

  • Сессия и PDO конфигурация вынесены в отдельный файл.
  • Проверка CSRF‑токена предотвращает атаки межсайтовой подделки запросов.
  • Валидация длины заголовка, пустоты сообщения, существования категории и авторизации.
  • Транзакция гарантирует, что тема и первое сообщение будут добавлены атомарно.
  • Используются подготовленные запросы (placeholders ?) для защиты от SQL‑инъекций.
  • Вывод ошибок и сохранение значений формы.

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

  • Отсутствие проверки прав доступа (любой пользователь может создать тему). Решение: проверять роль пользователя (например, `$_SESSION['role'] === 'admin'` или `'user'`).
  • Игнорирование экранирования вывода (XSS). В примере используется `htmlspecialchars()`.
  • Забывают вызвать `exit` после `header()`.
  • Ошибка тайм-аута сессии – сессия должна быть запущена до вывода HTML.

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

Как реализовать добавление темы без подготовленных запросов?

Устаревший подход с прямым встраиванием переменных в SQL (уже не рекомендуется). Пример:


<?php
$link = mysqli_connect('localhost', 'root', '', 'forum');
$title = $_POST['title'];
$content = $_POST['content'];
$user_id = $_SESSION['user_id'];
$category_id = $_POST['category_id'];

$query = "INSERT INTO topics (title, user_id, category_id, created_at) VALUES ('$title', $user_id, $category_id, NOW())";
$result = mysqli_query($link, $query);
?>
  

Create forum php (создание форума на php)

Пояснение: такой код крайне уязвим для SQL‑инъекций, любые кавычки в $title сломают запрос. Не используется транзакция и валидация.

Проблемы: очевидная SQL‑инъекция, отсутствие атомарности, нет защиты от XSS, сложность поддержки.

Единственный случай использования: быстрое прототипирование на локальном сервере без учёта безопасности.

Как организовать добавление темы с помощью класса-репозитория?

Объектно‑ориентированный подход. Создаётся класс `TopicRepository`, инкапсулирующий логику работы с БД.


<?php
class TopicRepository {
    private PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function create(array $data): int {
        // валидация и CSRF могут быть вынесены в отдельный сервис
        $stmt = $this->pdo->prepare('INSERT INTO topics (title, user_id, category_id, created_at) VALUES (:title, :user_id, :category_id, NOW())');
        $stmt->execute([
            ':title' => $data['title'],
            ':user_id' => $data['user_id'],
            ':category_id' => $data['category_id']
        ]);
        return (int)$this->pdo->lastInsertId();
    }

    public function addFirstPost(int $topicId, array $data): void {
        $stmt = $this->pdo->prepare('INSERT INTO posts (topic_id, user_id, content, created_at) VALUES (:topic_id, :user_id, :content, NOW())');
        $stmt->execute([':topic_id' => $topicId, ':user_id' => $data['user_id'], ':content' => $data['content']]);
    }
}

// Использование
$repo = new TopicRepository($pdo);
try {
    $pdo->beginTransaction();
    $topicId = $repo->create(['title' => $title, 'user_id' => $userId, 'category_id' => $catId]);
    $repo->addFirstPost($topicId, ['content' => $content, 'user_id' => $userId]);
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
}
?>
  

Forums user php (пользователи форума в php)

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

Цель: крупные проекты, где важна архитектура и тестируемость.

Ошибка: забывают использовать транзакции в вызывающем коде - тема может быть создана без сообщения. Решение: всегда оборачивать вызовы `create` и `addFirstPost` в транзакцию.

Как добавить тему через AJAX без перезагрузки страницы?

Используется JavaScript (fetch) и PHP‑обработчик, возвращающий JSON.


<?php
// ajax_create_topic.php
require 'config.php';
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['error' => 'Метод не поддерживается.']);
    exit;
}

// Валидация и CSRF, как в первом варианте
$data = json_decode(file_get_contents('php://input'), true);

// ... аналогичная обработка

if (empty($errors)) {
    // создание темы
    echo json_encode(['success' => true, 'topic_id' => $topic_id]);
} else {
    echo json_encode(['errors' => $errors]);
}
?>
  

Клиентская часть (JavaScript):


<script>
document.getElementById('topicForm').addEventListener('submit', function(e) {
    e.preventDefault();
    const formData = new FormData(this);
    const data = Object.fromEntries(formData.entries());
    fetch('ajax_create_topic.php', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(data)
    })
    .then(res => res.json())
    .then(result => {
        if (result.success) {
            location.href = 'viewtopic.php?t=' + result.topic_id;
        } else {
            alert(result.errors.join('\n'));
        }
    });
});
</script>
  

Пояснение: этот подход улучшает UX, но требует обработки ошибок на клиенте. Возможна проблема с CSRF – в примере используется JSON‑запрос, но токен передаётся в теле.

Типичная ошибка: не отправлять CSRF‑токен через JSON – надо отправлять его либо в заголовке, либо в теле. Решение: явно включить `csrf_token` в данные.

Цель: современные форумы с динамическими интерфейсами.

Расширенные примеры создания темы

Пример 1. Создание темы с загрузкой файла (изображения)

Пример

<?php
// сценарий: тема может содержать одно изображение
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // ... проверка CSRF, валидация
    $file = $_FILES['attachment'];
    $allowed = ['image/jpeg', 'image/png', 'image/gif'];
    $maxSize = 2 * 1024 * 1024; // 2 MB

    if ($file['error'] === UPLOAD_ERR_OK && in_array(mime_content_type($file['tmp_name']), $allowed) && $file['size'] <= $maxSize) {
        $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
        $newName = uniqid('topic_', true) . '.' . $ext;
        $uploadDir = __DIR__ . '/uploads/';
        if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
        move_uploaded_file($file['tmp_name'], $uploadDir . $newName);
        $filePath = '/uploads/' . $newName;
    } else {
        $errors[] = 'Ошибка загрузки файла. Разрешены JPEG, PNG, GIF до 2 МБ.';
    }

    // далее создание темы и запись пути в БД (например, таблица topic_files)
}
?>
Результат: Файл загружается в папку uploads, путь сохраняется в БД. Пример записи в БД:
+---------+-----------------------+
| topic_id| file_path             |
+---------+-----------------------+
| 123     | /uploads/topic_abc.jpg |
+---------+-----------------------+

Пример 2. Создание темы с предварительной проверкой на дубликат заголовка

Пример

<?php
// проверка уникальности заголовка в рамках категории
$checkStmt = $pdo->prepare('SELECT COUNT(*) FROM topics WHERE title = ? AND category_id = ?');
$checkStmt->execute([$title, $category_id]);
if ($checkStmt->fetchColumn() > 0) {
    $errors[] = 'Тема с таким заголовком уже существует в этой категории.';
}
?>
Результат: При попытке повторной вставки пользователь получает сообщение об ошибке.

Пример 3. Использование транзакции с сохранением нескольких сообщений при создании темы (например, опрос)

Пример

<?php
// структура: тема, первое сообщение, опрос (таблица polls)
$pdo->beginTransaction();

$stmt = $pdo->prepare('INSERT INTO topics ...');
$stmt->execute([...]);
$topicId = $pdo->lastInsertId();

$stmt = $pdo->prepare('INSERT INTO posts (topic_id, user_id, content, created_at) VALUES (?, ?, ?, NOW())');
$stmt->execute([$topicId, $userId, $firstPostContent]);

$stmt = $pdo->prepare('INSERT INTO polls (topic_id, question, created_at) VALUES (?, ?, NOW())');
$stmt->execute([$topicId, $pollQuestion]);
$pollId = $pdo->lastInsertId();

// добавить варианты ответов опроса
$stmt = $pdo->prepare('INSERT INTO poll_options (poll_id, option_text) VALUES (?, ?)');
foreach ($pollOptions as $opt) {
    $stmt->execute([$pollId, $opt]);
}

$pdo->commit();
?>
Результат: Создана тема, первое сообщение, опрос с вариантами ответов – все в одной транзакции.

Пример 4. Отправка уведомления администратору после создания темы

Пример

<?php
// после успешного создания темы
$subject = 'Новая тема: ' . $title;
$message = 'Пользователь ' . $userName . ' создал тему "' . $title . '" в категории ' . $categoryName;
$headers = 'From: forum@example.com' . "\r\n" . 'X-Mailer: PHP/' . phpversion();
mail($adminEmail, $subject, $message, $headers);
?>
Результат: Администратор получает email с информацией о новой теме.

Пример 5. Логирование действий (создание темы)

Пример

<?php
// запись в таблицу logs
$logStmt = $pdo->prepare('INSERT INTO logs (user_id, action, target_type, target_id, ip_address, created_at) VALUES (?, ?, ?, ?, ?, NOW())');
$logStmt->execute([$userId, 'create_topic', 'topic', $topicId, $_SERVER['REMOTE_ADDR']]);
?>
Таблица logs:
+---------+----------+-------------+-----------+----------------+---------------------+
| user_id | action   | target_type | target_id | ip_address     | created_at          |
+---------+----------+-------------+-----------+----------------+---------------------+
| 42      | create_topic | topic      | 123       | 192.168.1.100  | 2025-03-23 10:00:00 |
+---------+----------+-------------+-----------+----------------+---------------------+

Создание темы на форуме в PHP - comments

En
Viewtopic php t create (php)