Добавление валидации и обработки ошибок при сохранении произвольных полей?
У меня есть функция, которая определяет произвольное поле для типа записи. Допустим, поле называется "subhead".
Когда запись сохраняется, я хочу выполнить валидацию введенных данных и при необходимости отобразить сообщение об ошибке на странице редактирования записи. Что-то вроде:
// Обработка обновления записи
function wpse_update_post_custom_values($post_id, $post) {
// Выполняем проверку...
if($_POST['subhead'] != 'value i expect') {
// Добавляем ошибку
$errors->add('oops', 'Произошла ошибка.');
}
return $errors;
}
add_action('save_post','wpse_update_post_custom_values',1,2);
Я пытаюсь привязать это к действию save_post, но не могу понять, как обрабатывать ошибки. Похоже, что объект ошибок не передается в функцию, а если я создаю свой собственный объект WP_Error и возвращаю его, он не учитывается механизмом, который выводит ошибки на странице редактирования записи.
В настоящее время у меня есть сообщение об ошибке внутри моего произвольного meta box, но это не идеальное решение - я бы предпочел иметь большое красное сообщение вверху страницы, как обычно отображает WordPress.
Есть идеи?
ОБНОВЛЕНИЕ:
Основываясь на ответе @Denis, я попробовал несколько разных вариантов. Хранение ошибок как глобальной переменной не сработало, потому что WordPress выполняет перенаправление во время процесса save_post, что уничтожает глобальную переменную до того, как вы сможете её отобразить.
В итоге я сохранял их в meta-поле. Проблема в том, что вам нужно их очищать, иначе они не исчезнут при переходе на другую страницу, поэтому мне пришлось добавить еще одну функцию, привязанную к admin_footer, которая просто очищает ошибки.
Я не ожидал, что обработка ошибок для чего-то настолько распространенного (обновление записей) будет такой неуклюжей. Я что-то упускаю очевидное или это лучший подход?
// Обработка обновления записи
function wpse_5102_update_post_custom_values($post_id, $post) {
// Для хранения ошибок
$errors = false;
// Выполняем валидацию...
if($_POST['subhead'] != 'value i expect') {
// Добавляем ошибку
$errors .= 'Упс... произошла ошибка.';
}
update_option('my_admin_errors', $errors);
return;
}
add_action('save_post','wpse_5102_update_post_custom_values',1,2);
// Отображение ошибок
function wpse_5102_admin_notice_handler() {
$errors = get_option('my_admin_errors');
if($errors) {
echo '<div class="error"><p>' . $errors . '</p></div>';
}
}
add_action( 'admin_notices', 'wpse_5102_admin_notice_handler' );
// Очистка ошибок
function wpse_5102__clear_errors() {
update_option('my_admin_errors', false);
}
add_action( 'admin_footer', 'wpse_5102_clear_errors' );

Спасибо, что направили меня в этом направлении! В итоге я использовал мета-поле для хранения ошибок, так как у меня были проблемы с попытками сделать это через глобальную переменную или свойство. Сейчас обновляю свой ответ, чтобы объяснить, как я это делаю... дайте знать, если это то, что вы имели в виду, или есть более правильный способ, который я упускаю.

Да, что-то в этом роде. Хотя, если подумать, возможно, лучше хранить это в переменной сессии. Это позволит нескольким авторам одновременно редактировать записи. :-) Также, насколько я знаю, нельзя сохранить false в опции. Вместо этого используйте пустую строку.

Я рекомендую использовать сессии, так как это позволит избежать странных эффектов при одновременном редактировании несколькими пользователями. Вот что я делаю:
WordPress не запускает сессии самостоятельно. Поэтому вам нужно запустить сессию в вашем плагине, functions.php или даже wp-config.php:
if (!session_id())
session_start();
При сохранении записи добавляем ошибки и уведомления в сессию:
function my_save_post($post_id, $post) {
if($something_went_wrong) {
//Добавляем сообщение об ошибке, если что-то пошло не так
$_SESSION['my_admin_notices'] .= '<div class="error"><p>Возникла ошибка</p></div>';
return false; //может остановить дальнейшую обработку
}
if($somthing_to_notice) { //например, успешное сохранение
//Добавляем уведомление, если что-то требует внимания
$_SESSION['my_admin_notices'] .= '<div class="updated"><p>Запись обновлена</p></div>';
}
return true;
}
add_action('save_post','my_save_post');
Выводим уведомления и ошибки, а затем очищаем сообщения в сессии:
function my_admin_notices(){
if(!empty($_SESSION['my_admin_notices'])) print $_SESSION['my_admin_notices'];
unset ($_SESSION['my_admin_notices']);
}
add_action( 'admin_notices', 'my_admin_notices' );

исправление для версии с сессиями: при первом использовании переменной сессии не используйте .=, только =, если включить отладку, можно проверить почему...

Я тоже так делал, но если выпускать плагин для широкой аудитории таким образом, люди в итоге возненавидят вас за это. WordPress не инициализирует сессии, потому что он разработан как stateless и не нуждается в них, а некоторые странные настройки серверов могут сломать это. Используйте Transients API - http://codex.wordpress.org/Transients_API вместо сессий, и вы сохраните совместимость. Просто подумал, что стоит отметить причину, почему не стоит так делать здесь.

@pospi это, кажется, имеет схожие проблемы с исходным использованием функций get_option и update_option. Полагаю, решение состоит в добавлении ID текущего пользователя к ключу?

Основываясь на предложении pospi использовать транзиенты, я придумал следующее решение. Единственная проблема в том, что нет хука для размещения сообщения под h2
, где отображаются другие сообщения, поэтому мне пришлось использовать jQuery-костыль для этого.
Сначала сохраняем сообщение об ошибке в обработчике save_post
(или аналогичном). Я задаю короткое время жизни — 60 секунд, чтобы сообщение сохранялось ровно на время редиректа.
if($has_error)
{
set_transient( "acme_plugin_error_msg_$post_id", $error_msg, 60 );
}
Затем просто извлекаем это сообщение при следующей загрузке страницы и выводим его. Также удаляем сообщение, чтобы оно не отображалось повторно.
add_action('admin_notices', 'acme_plugin_show_messages');
function acme_plugin_show_messages()
{
global $post;
if ( false !== ( $msg = get_transient( "acme_plugin_error_msg_{$post->ID}" ) ) && $msg) {
delete_transient( "acme_plugin_error_msg_{$post->ID}" );
echo "<div id=\"acme-plugin-message\" class=\"error below-h2\"><p>$msg</p></div>";
}
}
Поскольку хук admin_notices
срабатывает до генерации основного контента страницы, уведомление не попадает в место, где отображаются другие сообщения при редактировании записи, поэтому я использовал этот jQuery-код для перемещения:
jQuery('h2').after(jQuery('#acme-plugin-message'));
Поскольку ID записи является частью имени транзиента, это решение должно работать в большинстве многопользовательских сред, за исключением случаев, когда несколько пользователей одновременно редактируют одну и ту же запись.

Можешь подробнее объяснить фразу "Поскольку ID записи является частью имени транзиента"? Я создал класс для обработки сообщений об ошибках, используя этот метод, но мне нужно передавать user_ID в конструктор. Использует ли Transient API user_id при хешировании ключа? (Спрашиваю, потому что в кодексе это, кажется, не упоминается)

Когда выполняется save_post
, запись уже сохранена в базе данных.
Анализируя исходный код WordPress, в частности функцию update_post()
в файле wp-includes/post.php
, нет встроенного способа перехватить запрос перед сохранением в базу данных.
Однако мы можем использовать хук pre_post_update
с функциями header()
и get_post_edit_link()
, чтобы предотвратить сохранение записи.
<?php
/**
* Выполняет валидацию перед сохранением/вставкой пользовательского типа записи
*/
function custom_post_site_save($post_id, $post_data) {
// Если это просто ревизия, ничего не делаем.
if (wp_is_post_revision($post_id))
return;
if ($post_data['post_type'] == 'my_custom_post_type') {
// Запрещаем заголовки записей короче 5 символов
if (strlen($post_data['post_title'] < 5)) {
header('Location: '.get_edit_post_link($post_id, 'redirect'));
exit;
}
}
}
add_action( 'pre_post_update', 'custom_post_site_save', 10, 2);
Если нужно уведомить пользователя об ошибке, проверьте этот gist: https://gist.github.com/Luc45/09f2f9d0c0e574c0285051b288a0f935

Спасибо за предложение! Что я упустил в вопросе (для простоты) — это то, что мне нужно обрабатывать ошибки загрузки файлов на стороне сервера. Все равно спасибо за предложение!

Валидация на javascript не защищает от некоторых атак, только серверная валидация является действительно безопасной. Кроме того, WordPress предлагает хорошие инструменты для проверки пользовательских данных. Но вы правы, если это просто проверка значений перед отправкой данных на сервер, это может сэкономить время при небольшой нагрузке на сервер ^^

Пытаясь использовать приведённый выше скрипт, я столкнулся со странной проблемой. На экране редактирования отображаются два сообщения после обновления записи. Одно показывает состояние содержимого из предыдущего сохранения, а другое — из текущего. Например, если я правильно сохраняю запись, а затем делаю ошибку, первое сообщение — "ошибка", а второе — "ок", хотя они генерируются в одно и то же время. Если я изменю скрипт и добавлю только одно сообщение (например, "ошибка"), инициирую одно обновление с "ошибка" и затем другое с "ок", сообщение "ошибка" остаётся (отображается во второй раз). Мне приходится сохранять с "ок" ещё раз, чтобы избавиться от него. Я действительно не понимаю, в чём проблема, я проверил это на трёх разных локальных серверах, и на каждом из них наблюдается та же самая ошибка. Если у кого-то есть идеи или предложения, пожалуйста, помогите!

Я провел дополнительные тесты более простой, второй версии скрипта, о которой упоминал выше, и кажется, что если сообщение об "ошибке" действительно добавляется в массив сессии, оно отображается на экране редактирования. Если сообщения нет (все "ок") и предыдущее сообщение было об ошибке, оно появляется на экране. Что странно, оно генерируется в момент сохранения (не кэшируется) - я проверил это, используя date() в теле сообщения об ошибке. Сейчас я совершенно сбит с толку.

Я разработал плагин, который добавляет мгновенную обработку ошибок на экранах редактирования записей и предотвращает публикацию постов до тех пор, пока не будут заполнены обязательные поля:
https://github.com/interconnectit/required-fields
Он позволяет сделать любые поля записи обязательными, но вы также можете использовать предоставляемый API, чтобы сделать обязательными любые пользовательские поля с настраиваемым сообщением об ошибке и функцией валидации. По умолчанию проверяется, пустое поле или нет.
