wpquiz.ru wordpress WPQuiz.ru

Как сохранять ответы квизов в Custom Post Type WordPress и не потерять данные

Если квиз уже приносит лиды, а ответы лежат только в transient, в письмах или в стороннем сервисе, рано или поздно появится проблема: данные сложно фильтровать, нельзя нормально выгрузить историю, а при сбое интеграции часть заявок теряется. Для таких сценариев удобнее сохранять ответ в отдельный custom post type и складывать детали в post meta.

Это не универсальное решение для всех проектов. Но если вам нужен понятный архив ответов внутри WordPress, ручная модерация, экспорт через админку и возможность связать ответ с пользователем или заказом WooCommerce, схема с CPT обычно оказывается практичнее, чем хранение в одной большой опции или в разрозненных таблицах.

Когда CPT для ответов действительно уместен

Подход подходит, если ответы нужно:

  • просматривать в админке как отдельные записи;
  • фильтровать по статусу, квизу, пользователю или дате;
  • передавать дальше в CRM или в WooCommerce как источник лида;
  • сохранять вместе с метаданными: IP, UTM, результат, выбранные варианты;
  • не терять при отключении внешнего сервиса.

Если же квиз генерирует десятки тысяч записей в день и нужен сложный аналитический слой, лучше сразу смотреть в сторону отдельной таблицы. CPT в таком случае быстро упрётся в ограничения админки и мета-запросов.

Диагностика: почему ответы сейчас теряются или плохо ищутся

Перед внедрением проверьте, где именно ломается цепочка. Обычно проблема не в самом сохранении, а в том, что данные не нормализованы или не проходят валидацию.

Что проверить в первую очередь

  • Есть ли у формы nonce и проверяется ли он на сервере.
  • Не отправляются ли ответы только через admin-ajax.php без резервного сохранения.
  • Не пишете ли вы всё в одну опцию через update_option(), что неудобно для поиска и опасно для роста объёма.
  • Не хранится ли массив ответов в одном meta-поле без структуры и индексов.
  • Есть ли у записи понятный статус: черновик, новый, обработан, отклонён.

Если ответ уже приходит на сервер, но не появляется в админке, проверьте права пользователя, регистрацию CPT и то, не скрыт ли он параметром show_ui.

Пошаговое решение: регистрируем CPT и сохраняем ответ

Ниже рабочая схема: отдельный тип записи quiz_answer, метаполя для данных и простой AJAX-обработчик. Код можно положить в мини-плагин или в functions.php, но для продакшена лучше отдельный плагин, чтобы не потерять логику при смене темы.

1. Регистрируем тип записи

<?php
add_action('init', function () {
    register_post_type('quiz_answer', [
        'labels' => [
            'name'          => 'Ответы квизов',
            'singular_name'  => 'Ответ квиза',
            'add_new_item'   => 'Добавить ответ',
            'edit_item'      => 'Редактировать ответ',
        ],
        'public'       => false,
        'show_ui'      => true,
        'show_in_menu' => true,
        'supports'     => ['title'],
        'capability_type' => 'post',
        'map_meta_cap' => true,
        'menu_icon'    => 'dashicons-forms',
    ]);
});

Здесь запись не публикуется на фронтенде. Она нужна как контейнер для хранения и админского просмотра. Заголовок можно формировать из имени пользователя, email или названия квиза.

2. Принимаем данные и создаём запись

Обработчик ниже проверяет nonce, чистит входные данные и сохраняет массив ответов в мета-поле. Для примера используется wp_ajax_ и wp_ajax_nopriv_, чтобы форма работала и для гостей.

<?php
add_action('wp_ajax_save_quiz_answer', 'wpquiz_save_quiz_answer');
add_action('wp_ajax_nopriv_save_quiz_answer', 'wpquiz_save_quiz_answer');

function wpquiz_save_quiz_answer() {
    check_ajax_referer('quiz_answer_nonce', 'nonce');

    $quiz_id = isset($_POST['quiz_id']) ? absint($_POST['quiz_id']) : 0;
    $name    = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
    $email   = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
    $answers = isset($_POST['answers']) ? (array) wp_unslash($_POST['answers']) : [];

    if (!$quiz_id || empty($answers)) {
        wp_send_json_error(['message' => 'Недостаточно данных для сохранения.'], 400);
    }

    $post_id = wp_insert_post([
        'post_type'   => 'quiz_answer',
        'post_status' => 'publish',
        'post_title'  => sprintf('Квиз #%d — %s', $quiz_id, $email ? $email : $name),
    ], true);

    if (is_wp_error($post_id)) {
        wp_send_json_error(['message' => $post_id->get_error_message()], 500);
    }

    update_post_meta($post_id, '_quiz_id', $quiz_id);
    update_post_meta($post_id, '_quiz_name', $name);
    update_post_meta($post_id, '_quiz_email', $email);
    update_post_meta($post_id, '_quiz_answers', array_map('sanitize_text_field', $answers));
    update_post_meta($post_id, '_quiz_ip', isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '');
    update_post_meta($post_id, '_quiz_user_agent', isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '');

    wp_send_json_success([
        'post_id' => $post_id,
        'message' => 'Ответ сохранён.',
    ]);
}

Если ответы содержат вложенные структуры, не пытайтесь хранить их как сырой JSON без проверки. Лучше заранее привести массив к ожидаемому виду: вопрос, ответ, тип поля, порядок.

3. Передаём nonce и отправляем запрос с фронтенда

Для формы нужен nonce и URL обработчика. Это можно сделать через wp_localize_script() или wp_add_inline_script(). Ниже минимальный вариант.

<?php
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('quiz-answer', get_stylesheet_directory_uri() . '/js/quiz-answer.js', ['jquery'], '1.0', true);
    wp_localize_script('quiz-answer', 'QuizAnswerData', [
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'nonce'   => wp_create_nonce('quiz_answer_nonce'),
    ]);
});

А в quiz-answer.js отправляйте данные обычным POST. Если у вас уже есть AJAX-логика квиза, добавьте только сохранение результата, а не переписывайте весь сценарий.

Как проверить, что сохранение работает

После внедрения не ограничивайтесь сообщением «успешно». Проверьте цепочку целиком:

  1. Отправьте тестовый ответ с гостевого окна браузера.
  2. Убедитесь, что в ответе AJAX приходит success: true и post_id.
  3. Откройте админку WordPress и найдите запись в разделе «Ответы квизов».
  4. Проверьте метаполя через get_post_meta() или через плагин просмотра метаданных.
  5. Сделайте повторную отправку с теми же данными и убедитесь, что не создаются дубликаты, если это не требуется по логике проекта.

Если хотите быстро посмотреть, что реально записалось, временно добавьте логирование:

<?php
error_log('Quiz answer saved: ' . print_r([
    'post_id' => $post_id,
    'quiz_id' => $quiz_id,
    'email'   => $email,
], true));

После проверки лог лучше убрать или перевести на нормальный канал отладки, чтобы не засорять error log на продакшене.

Сравнение подходов: CPT, опция и отдельная таблица

ПодходКогда удобенМинус
CPT + post metaНужна админка, ручной просмотр, умеренный объёмСложнее строить тяжёлую аналитику
Одна опцияОчень мало данных, быстрый прототипПлохо ищется, неудобно масштабируется
Отдельная таблицаМного ответов, фильтрация, отчёты, интеграцииНужно писать свою схему и запросы

Если проект уже вырос, а ответы нужны для отчётов и сегментации, отдельная таблица будет честнее. Но если задача — быстро и надёжно хранить ответы внутри WordPress, CPT закрывает большинство бытовых сценариев без лишней сложности.

Частые ошибки и как их исправить

Не проверяют nonce

Без nonce форму легко дергать извне. В результате в базу летят мусорные записи или запросы от ботов. Исправление простое: check_ajax_referer() на сервере и передача nonce на фронтенде.

Сохраняют всё в один массив без нормализации

Если вопрос в одном проекте называется q1, а в другом question_1, потом невозможно строить выборки. Лучше заранее зафиксировать структуру: идентификатор вопроса, текст, значение ответа.

Используют sanitize_text_field() для всего подряд

Это не универсальный фильтр. Для email нужен sanitize_email(), для чисел — absint(), для массивов — проход по элементам. Иначе можно потерять данные или сохранить их в неверном виде.

Ставят post_status = publish без необходимости

Если записи не должны быть видны никому, кроме админов, используйте private или отдельный статус. Это особенно важно, если в ответе есть персональные данные.

Безопасность и производительность

Ответы квизов часто содержат email, телефон, иногда адрес или комментарий. Значит, к ним нужно относиться как к чувствительным данным.

  • не храните лишнее: если IP не нужен для задачи, не записывайте его;
  • ограничьте доступ к CPT только нужным ролям;
  • не выводите метаданные без экранирования;
  • не отправляйте весь массив ответа в HTML без фильтрации;
  • если ответов много, не делайте тяжёлые meta_query на каждом запросе админки.

Для ускорения админки полезно вынести часто используемые поля в отдельные мета-ключи, а не хранить всё в одном большом сериализованном массиве. Это упростит поиск и уменьшит стоимость чтения данных.

Если вам нужно не только хранение, но и нормальная аналитика, можно оставить CPT как интерфейс для модерации, а для отчётов дублировать минимальный набор полей в отдельную таблицу или отправлять события в внешнюю систему. Такой гибрид часто удобнее, чем пытаться выжать из одной схемы всё сразу.

×
до 3225₽

Продавай темы и плагины WordPress!

Лови с каждой продажи

Начать ⋙