Если квиз уже приносит лиды, а ответы лежат только в 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-логика квиза, добавьте только сохранение результата, а не переписывайте весь сценарий.
Как проверить, что сохранение работает
После внедрения не ограничивайтесь сообщением «успешно». Проверьте цепочку целиком:
- Отправьте тестовый ответ с гостевого окна браузера.
- Убедитесь, что в ответе AJAX приходит
success: trueиpost_id. - Откройте админку WordPress и найдите запись в разделе «Ответы квизов».
- Проверьте метаполя через
get_post_meta()или через плагин просмотра метаданных. - Сделайте повторную отправку с теми же данными и убедитесь, что не создаются дубликаты, если это не требуется по логике проекта.
Если хотите быстро посмотреть, что реально записалось, временно добавьте логирование:
<?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 как интерфейс для модерации, а для отчётов дублировать минимальный набор полей в отдельную таблицу или отправлять события в внешнюю систему. Такой гибрид часто удобнее, чем пытаться выжать из одной схемы всё сразу.