Agregar validación y manejo de errores al guardar campos personalizados
Tengo una función que define un campo personalizado en un tipo de entrada. Digamos que el campo es "subhead".
Cuando se guarda la entrada, quiero realizar alguna validación en el input y mostrar un mensaje de error en la pantalla de edición si es necesario. Algo como:
// Manejar actualización de entrada
function wpse_update_post_custom_values($post_id, $post) {
// Hacer algunas verificaciones...
if($_POST['subhead'] != 'valor que espero') {
// Agregar un error aquí
$errors->add('oops', 'Hubo un error.');
}
return $errors;
}
add_action('save_post','wpse_update_post_custom_values',1,2);
Estoy tratando de conectar esto a la acción save_post, pero no puedo averiguar cómo manejar los errores. No parece haber un objeto de error pasado a la función, y si creo mi propio objeto WP_Error y lo devuelvo, no es respetado por el mecanismo que muestra los errores en la página de edición.
Actualmente tengo un mensaje de error en la página dentro de mi meta box personalizado, pero esto no es lo ideal - preferiría tener un error grande y rojo en la parte superior como normalmente muestra WordPress.
¿Alguna idea?
ACTUALIZACIÓN:
Basado en la respuesta de @Denis, probé varias cosas diferentes. Almacenar errores como una variable global no funcionó, porque WordPress hace una redirección durante el proceso save_post, lo que elimina la global antes de que puedas mostrarla.
Terminé almacenándolos en un campo meta. El problema con esto es que necesitas limpiarlos, o no desaparecerán cuando navegues a otra página, así que tuve que agregar otra función conectada al admin_footer que simplemente limpia los errores.
No hubiera esperado que el manejo de errores para algo tan común (actualizar entradas) fuera tan complicado. ¿Me estoy perdiendo algo obvio o este es el mejor enfoque?
// Manejar actualización de entrada
function wpse_5102_update_post_custom_values($post_id, $post) {
// Para mantener los errores
$errors = false;
// Hacer alguna validación...
if($_POST['subhead'] != 'valor que espero') {
// Agregar un error aquí
$errors .= 'ups... hubo un error.';
}
update_option('my_admin_errors', $errors);
return;
}
add_action('save_post','wpse_5102_update_post_custom_values',1,2);
// Mostrar cualquier error
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' );
// Limpiar cualquier error
function wpse_5102__clear_errors() {
update_option('my_admin_errors', false);
}
add_action( 'admin_footer', 'wpse_5102_clear_errors' );

¡Gracias por señalarme esta dirección! Terminé usando un meta para almacenar errores, porque tuve problemas intentando hacerlo como una variable global o una propiedad. Estoy actualizando mi respuesta ahora mismo para explicar cómo lo estoy haciendo... por favor, déjame saber si esto es el tipo de cosa que estabas sugiriendo, o si hay una mejor manera que no estoy entendiendo.

Ese tipo de cosa, sí. Aunque, pensándolo mejor, tal vez almacenarlo en una variable de sesión. Esto, para permitir que múltiples autores editen publicaciones al mismo tiempo. :-) Además, creo que no es posible almacenar false en una opción. Almacena una cadena vacía en su lugar.

Sugiero usar sesiones ya que esto no creará efectos extraños cuando dos usuarios editen al mismo tiempo. Esto es lo que hago:
WordPress no inicia sesiones por defecto. Así que necesitas iniciar una sesión en tu plugin, functions.php o incluso wp-config.php:
if (!session_id())
session_start();
Al guardar el post, agrega errores y notificaciones a la sesión:
function my_save_post($post_id, $post) {
if($something_went_wrong) {
//Agregar notificación de error si algo salió mal
$_SESSION['my_admin_notices'] .= '<div class="error"><p>Algo salió mal</p></div>';
return false; //podría detener el procesamiento aquí
}
if($somthing_to_notice) { //ej. guardado exitoso
//Agregar notificación si algo salió mal
$_SESSION['my_admin_notices'] .= '<div class="updated"><p>Post actualizado</p></div>';
}
return true;
}
add_action('save_post','my_save_post');
Mostrar notificaciones y errores y luego limpiar los mensajes en la sesión:
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' );

solución para la versión de sesión: la primera vez que uses la variable de sesión no uses .= solo = si activas la depuración, puedes ver por qué...

Yo también he hecho esto, pero si publicas un plugin para una audiencia amplia de esa manera, la gente terminará odiándote por ello. WordPress no inicia sesiones porque está diseñado para ser sin estado y no necesitarlas, y algunas configuraciones extrañas de servidor pueden romperlo. Usa la API de transitorios - http://codex.wordpress.org/Transients_API en lugar de sesiones y mantendrás la compatibilidad. Solo pensé que valía la pena señalar una razón para no hacer esto aquí.

@pospi esto parece tener problemas similares al uso original de las funciones get_option y update_option. Así que supongo que la solución sería añadir el ID del usuario actual a la clave?

Basado en la sugerencia de pospi para usar transients, se me ocurrió lo siguiente. El único problema es que no hay un hook para colocar el mensaje debajo del h2
donde van otros mensajes, así que tuve que usar un truco con jQuery para colocarlo ahí.
Primero, guarda el mensaje de error durante tu manejador save_post
(o similar). Le doy una vida corta de 60 segundos, así que estará allí solo el tiempo suficiente para que ocurra la redirección.
if($has_error)
{
set_transient( "acme_plugin_error_msg_$post_id", $error_msg, 60 );
}
Luego, simplemente recupera ese mensaje de error en la siguiente carga de página y muéstralo. También lo elimino para que no se muestre dos veces.
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>";
}
}
Dado que admin_notices
se ejecuta antes de que se genere el contenido principal de la página, el aviso no aparece donde van otros mensajes de edición de entradas, así que tuve que usar este jQuery para moverlo ahí:
jQuery('h2').after(jQuery('#acme-plugin-message'));
Como el ID de la entrada es parte del nombre del transient, esto debería funcionar en la mayoría de entornos multi-usuario excepto cuando varios usuarios están editando la misma entrada concurrentemente.

¿Podrías elaborar sobre "Dado que el ID del post es parte del nombre del transitorio"? Creé una clase para manejar mensajes de error usando esta técnica pero requiero que mi constructor pase un user_ID. ¿La API de transitorios usa el user_id al hacer hash de la clave? (pregunto porque el codex no parece mencionar esto)

Cuando se ejecuta save_post
, el post ya ha sido guardado en la base de datos.
Al revisar el código núcleo de WordPress, específicamente en la función update_post()
del archivo wp-includes/post.php
, no existe una forma incorporada de interceptar una solicitud antes de que se guarde en la base de datos.
Sin embargo, podemos engancharnos a pre_post_update
y usar header()
junto con get_post_edit_link()
para evitar que el post se guarde.
<?php
/**
* Realiza validación antes de guardar/insertar un custom post type
*/
function custom_post_site_save($post_id, $post_data) {
// Si esto es solo una revisión, no hacer nada
if (wp_is_post_revision($post_id))
return;
if ($post_data['post_type'] == 'my_custom_post_type') {
// Rechazar títulos con menos de 5 caracteres
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);
Si deseas notificar al usuario sobre el error, revisa este gist: https://gist.github.com/Luc45/09f2f9d0c0e574c0285051b288a0f935

¡Gracias por la sugerencia! Lo que dejé fuera de la pregunta (por simplificar) es que estoy intentando manejar errores de carga de archivos, así que necesita ser del lado del servidor. ¡Gracias de todos modos por la sugerencia!

La validación con javascript no previene de algunos ataques, la validación del lado del servidor es la única segura. Además, WordPress ofrece buenas herramientas para validar datos de usuario. Pero tienes razón, si solo se trata de verificar algunos valores antes de enviar datos al servidor, puedes ahorrar algo de tiempo en servidores con baja capacidad ^^

Al intentar usar el script anterior, me encontré con un problema extraño. Se muestran dos mensajes en la pantalla de edición después de actualizar la publicación. Uno muestra el estado del contenido del guardado anterior y otro del actual. Por ejemplo, si guardo la publicación correctamente y luego cometo un error, el primero muestra "error" y el segundo "ok", aunque se generan al mismo tiempo. Si modifico el script y solo agrego un mensaje (por ejemplo, "error"), inicio una actualización con "error" y luego otra con "ok", el mensaje de "error" permanece (se muestra por segunda vez). Debo guardar con "ok" una vez más para eliminarlo. Realmente no sé qué está mal, lo he probado en tres servidores locales diferentes y ocurre el mismo problema en cada uno. Si alguien tiene alguna idea o sugerencia, ¡por favor ayude!

Hice algunas pruebas más de la versión más simple y segunda del script que mencioné arriba y parece que si el mensaje de "error" realmente se agrega al arreglo de sesión, se muestra en la pantalla de edición. Si no hay mensaje (todo está "ok") y el mensaje anterior era un error, aparece en la pantalla. Lo extraño es que se genera al momento de guardar (no está cachead) - lo verifiqué usando date() en el cuerpo del mensaje de error. Ahora estoy totalmente confundido.

He escrito un plugin que añade un manejo de errores rápido para las pantallas de edición de entradas y evita que las entradas se publiquen hasta que se completen los campos obligatorios:
https://github.com/interconnectit/required-fields
Permite hacer obligatorios cualquier campo de entrada, pero también puedes usar la API que proporciona para hacer que cualquier campo personalizado sea obligatorio con un mensaje de error personalizable y una función de validación. Por defecto, comprueba si el campo está vacío o no.
