Организация просмотра теста в обучающем модуле на PHP

Раздел: Образовательные модули -> Обучение

Реализация просмотра теста на PHP

Самый эффективный способ: единая страница с PDO и обработкой POST

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

Рассмотрим пример, где все вопросы теста отображаются сразу, а проверка происходит после отправки формы. Такой подход минимизирует количество запросов к серверу и упрощает код.

Шаги:

  1. Создание таблицы в MySQL с вопросами, вариантами и правильными ответами.
  2. Подключение к базе через PDO.
  3. Получение всех вопросов и вывод их в форме.
  4. Обработка POST-запроса, подсчет баллов и вывод результата.

Пример кода (файл view_quiz.php):


<?php
// Подключение к БД
try {
    $pdo = new PDO('mysql:host=localhost;dbname=quiz;charset=utf8', 'user', 'pass');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die('Ошибка соединения: ' . $e->getMessage());
}

// Получение всех вопросов
$stmt = $pdo->query("SELECT * FROM questions");
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Обработка ответов
$result = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['quiz'])) {
    $score = 0;
    $total = count($questions);
    foreach ($questions as $q) {
        $correct = $q['correct'];
        $userAnswer = isset($_POST['q' . $q['id']]) ? $_POST['q' . $q['id']] : '';
        if ($userAnswer == $correct) {
            $score++;
        }
    }
    $result = "<div class='alert alert-info'>Вы набрали {$score} из {$total} баллов.</div>";
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Тест</title>
</head>
<body>
<h2>Пройти тест</h2>
<?= $result ?>
<form method="post">
    <?php foreach ($questions as $q): ?>
        <div class="question">
            <p><span class="fw-bold">Вопрос:</span> <?= htmlspecialchars($q['question']) ?></p>
            <label><input type="radio" name="q<?= $q['id'] ?>" value="1"> <?= htmlspecialchars($q['opt1']) ?></label><br>
            <label><input type="radio" name="q<?= $q['id'] ?>" value="2"> <?= htmlspecialchars($q['opt2']) ?></label><br>
            <label><input type="radio" name="q<?= $q['id'] ?>" value="3"> <?= htmlspecialchars($q['opt3']) ?></label><br>
        </div>
    <?php endforeach; ?>
    <button type="submit" name="quiz">Отправить</button>
</form>
</body>
</html>
  

View quiz php (просмотр теста php)

Типичные проблемы и их решения:

  • SQL-инъекции - в примере используется PDO и подготовленные запросы не нужны, но при динамической выборке всегда используйте подготовленные выражения.
  • XSS-уязвимость - все данные из БД экранируются функцией htmlspecialchars().
  • Потеря данных формы после отправки - после обработки ответов форма очищается. Для сохранения выбранных вариантов можно хранить их в сессии или передавать обратно.

Вариант 1: Использование сессии для пошагового прохождения

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

Если тест длинный, пользователю удобнее видеть по одному вопросу на странице. Ответы сохраняются в сессии. Пример:


session_start();
// Инициализация номера вопроса
if (!isset($_SESSION['current'])) {
    $_SESSION['current'] = 0;
    $_SESSION['answers'] = [];
}

// Обработка ответа
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // сохранение ответа
    $_SESSION['answers'][$_SESSION['current']] = $_POST['answer'];
    $_SESSION['current']++;
    // если вопросов больше нет - завершение
    // ...
}
  

уроки php mysql (уроки по php и mysql)

Такой подход требует отслеживания состояния и управления навигацией.

Проблемы: Завершение сессии или истечение времени может привести к потере данных. Необходимо информировать пользователя о сроках действия.

Вариант 2: AJAX-загрузка вопросов без перезагрузки страницы

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

На стороне PHP готовим endpoint, который отдаёт один вопрос в формате JSON. Клиентский JavaScript подгружает следующий вопрос по мере ответа пользователя.


// get_question.php?id=<номер>
$id = intval($_GET['id']);
$stmt = $pdo->prepare("SELECT * FROM questions WHERE id = ?");
$stmt->execute([$id]);
$q = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode($q);
  

На фронте используется fetch() для получения данных и рендеринга без перезагрузки.

Ошибки: Необходимо обрабатывать случай, когда вопрос не найден. Корректно настраивать заголовки CORS, если API на другом домене.

Вариант 3: Использование шаблонизатора (Twig) для отделения логики от представления

Как организовать чистый код с помощью шаблонизатора?

Установка Twig, создание шаблона quiz.html.twig, передача данных из контроллера.


// index.php
require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader);
echo $twig->render('quiz.html.twig', ['questions' => $questions, 'result' => $result]);
  

Шаблон содержит циклы и условия, что делает HTML чистым.

Типичная ошибка: Неправильные пути к шаблонам или отсутствие кэша. Для продакшена рекомендуется включить кэширование Twig.

Вариант 4: Защита от повторной отправки (CSRF-токен)

Как предотвратить повторную отправку формы теста?

Генерация уникального токена, хранение его в сессии, проверка при POST.


session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
// В форме: <input type="hidden" name="token" value="<?= $_SESSION['token'] ?>">
// При обработке:
if (!hash_equals($_SESSION['token'], $_POST['token'])) {
    die('Недействительный токен');
}
unset($_SESSION['token']); // однократное использование
  

Возможная проблема: Истечение сессии приводит к потере токена, и пользователь не может отправить форму. Рекомендуется отображать понятное сообщение.

Расширенные примеры с кодом и результатом

Пример 1: Полный тест с таймером и автоматической отправкой

Как реализовать обратный отсчёт времени и принудительную отправку?

PHP генерирует временную метку окончания, JavaScript отслеживает оставшееся время и отправляет форму при истечении.

Пример

// timer.php
session_start();
$timeLimit = 300; // 5 минут в секундах
if (!isset($_SESSION['end_time'])) {
    $_SESSION['end_time'] = time() + $timeLimit;
}
$remaining = $_SESSION['end_time'] - time();
?>
<script>
var remaining = <?= $remaining ?>;
var timer = setInterval(function() {
    remaining--;
    document.getElementById('timer').innerText = remaining;
    if (remaining <= 0) {
        clearInterval(timer);
        document.getElementById('quizForm').submit();
    }
}, 1000);
</script>
<span id="timer"></span> секунд осталось
// Результат: на странице отображается таймер обратного отсчёта, по истечении форма отправляется автоматически.

Пример 2: Вывод результатов с детализацией по каждому вопросу

Как показать пользователю его ответы и правильные варианты после проверки?

Массив правильных ответов хранится в сессии после проверки, либо считывается из БД. При выводе добавляем стили для верных/неверных ответов.

Пример

// Часть обработчика
$userAnswers = $_POST;
$results = [];
foreach ($questions as $q) {
    $correct = $q['correct'];
    $userAnswer = isset($userAnswers['q' . $q['id']]) ? $userAnswers['q' . $q['id']] : '';
    $isCorrect = ($userAnswer == $correct);
    $results[] = [
        'question' => $q['question'],
        'user_answer' => $userAnswer,
        'correct_answer' => $correct,
        'is_correct' => $isCorrect
    ];
}
$_SESSION['results'] = $results;
// На странице результата можно вывести таблицу: номер вопроса, ответ пользователя, правильный ответ, иконка успеха/неудачи.

Пример 3: Импорт теста из JSON и динамический рендеринг

Как загрузить вопросы из внешнего файла JSON?

Используем file_get_contents и json_decode для чтения, затем рендерим аналогично.

Пример

$json = file_get_contents('questions.json');
$questions = json_decode($json, true);
// Далее такой же код, как с БД, только без PDO.
// Вопросы отобразятся корректно. JSON должен иметь структуру:
[{"id":1,"question":"...","opt1":"...","opt2":"...","correct":1}, ...]

Пример 4: Асинхронная проверка ответов один за другим

Как проверять каждый ответ сразу после выбора (без кнопки 'Отправить')?

Используется AJAX: onChange на radio-кнопках отправляет выбранный ответ на сервер, сервер возвращает правильность, и на странице отображается зелёный/красный индикатор.

Пример

// quiz.js
document.querySelectorAll('input[type="radio"]').forEach(input => {
    input.addEventListener('change', function() {
        const formData = new FormData();
        formData.append('question_id', this.name.replace('q', ''));
        formData.append('answer', this.value);
        fetch('check_answer.php', { method: 'POST', body: formData })
        .then(res => res.json())
        .then(data => {
            if (data.correct) {
                this.parentElement.style.color = 'green';
            } else {
                this.parentElement.style.color = 'red';
            }
        });
    });
});
// После выбора ответа цвет меняется мгновенно, пользователь сразу видит результат.

Пример 5: Использование готового PHP-фреймворка (Laravel) для тестов

Как упростить разработку с помощью Laravel?

Создание модели Question, контроллера QuizController, миграции и Blade-шаблонов. Пример маршрута и метода:

Пример

// routes/web.php
Route::get('/quiz', [QuizController::class, 'show']);
Route::post('/quiz', [QuizController::class, 'check']);

// QuizController.php
public function show() {
    $questions = Question::all();
    return view('quiz.show', compact('questions'));
}
// В Blade-шаблоне используется цикл @foreach и форма с CSRF. Результат возвращается с сессионным сообщением.

Просмотр теста PHP - comments

En
View quiz php (php)