Error: Página de Opciones No Encontrada al Enviar Configuración en Plugin OOP
Estoy desarrollando un plugin usando el repositorio Boilerplate de Tom McFarlin como plantilla, que utiliza prácticas POO. He estado tratando de averiguar exactamente por qué no puedo enviar correctamente mi configuración. He intentado establecer el atributo action como una cadena vacía como se sugiere en otra pregunta por aquí, pero eso no ha ayudado...
A continuación está la configuración general del código que estoy usando...
El Formulario (/views/admin.php):
<div class="wrap">
<h2><?php echo esc_html( get_admin_page_title() ); ?></h2>
<form action="options.php" method="post">
<?php
settings_fields( $this->plugin_slug );
do_settings_sections( $this->plugin_slug );
submit_button( 'Guardar Configuración' );
?>
</form>
</div>
Para el siguiente código, asume que todas las callbacks para add_settings_field() y add_settings_section() existen, excepto 'option_list_selection'.
La Clase Admin del Plugin(/{plugin_name}-class-admin.php):
namespace wp_plugin_name;
class Plugin_Name_Admin
{
/**
* Nota: Algunas partes del código de la clase y funciones de método faltan por brevedad
* Avísame si necesitas más información...
*/
private function __construct()
{
$plugin = Plugin_Name::get_instance();
$this->plugin_slug = $plugin->get_plugin_slug();
$this->friendly_name = $plugin->get_name(); // Obtener nombre "amigable" presentable
// Agrega todas las opciones para la configuración administrativa
add_action( 'admin_init', array( $this, 'plugin_options_init' ) );
// Agrega la página de opciones y el elemento del menú
add_action( 'admin_menu', array( $this, 'add_plugin_admin_menu' ) );
}
public function add_plugin_admin_menu()
{
// Agregar una Página de Opciones
$this->plugin_screen_hook_suffix =
add_options_page(
__( $this->friendly_name . " Opciones", $this->plugin_slug ),
__( $this->friendly_name, $this->plugin_slug ),
"manage_options",
$this->plugin_slug,
array( $this, "display_plugin_admin_page" )
);
}
public function display_plugin_admin_page()
{
include_once( 'views/admin.php' );
}
public function plugin_options_init()
{
// Actualizar Configuración
add_settings_section(
'maintenance',
'Mantenimiento',
array( $this, 'maintenance_section' ),
$this->plugin_slug
);
// Opción de Verificar Actualizaciones
register_setting(
'maintenance',
'plugin-name_check_updates',
'wp_plugin_name\validate_bool'
);
add_settings_field(
'check_updates',
'¿Debería ' . $this->friendly_name . ' Buscar Actualizaciones?',
array( $this, 'check_updates_field' ),
$this->plugin_slug,
'maintenance'
);
// Opción de Período de Actualización
register_setting(
'maintenance',
'plugin-name_update_period',
'wp_plugin_name\validate_int'
);
add_settings_field(
'update_frequency',
'¿Con qué frecuencia debería ' . $this->friendly_name . ' buscar actualizaciones?',
array( $this, 'update_frequency_field' ),
$this->plugin_slug,
'maintenance'
);
// Configuraciones de Opciones del Plugin
add_settings_section(
'category-option-list', 'Lista de Opciones del Widget',
array( $this, 'option_list_section' ),
$this->plugin_slug
);
}
}
Algunas Actualizaciones Solicitadas:
Cambiar el atributo action a:
<form action="../../options.php" method="post">
...simplemente resulta en un Error 404. A continuación se muestra el extracto de los Registros de Apache. Tenga en cuenta que los scripts predeterminados de WordPress y las colas CSS están eliminados:
# Cambiado a ../../options.php
127.0.0.1 - - [01/Apr/2014:15:59:43 -0400] "GET /wp-admin/options-general.php?page=pluginname-widget HTTP/1.1" 200 18525
127.0.0.1 - - [01/Apr/2014:15:59:43 -0400] "GET /wp-content/plugins/PluginName/admin/assets/css/admin.css?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:15:59:43 -0400] "GET /wp-content/plugins/PluginName/admin/assets/js/admin.js?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:15:59:52 -0400] "POST /options.php HTTP/1.1" 404 1305
127.0.0.1 - - [01/Apr/2014:16:00:32 -0400] "POST /options.php HTTP/1.1" 404 1305
#Cambiado a options.php
127.0.0.1 - - [01/Apr/2014:16:00:35 -0400] "GET /wp-admin/options-general.php?page=pluginname-widget HTTP/1.1" 200 18519
127.0.0.1 - - [01/Apr/2014:16:00:35 -0400] "GET /wp-content/plugins/PluginName/admin/assets/css/admin.css?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:16:00:35 -0400] "GET /wp-content/plugins/PluginName/admin/assets/js/admin.js?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:16:00:38 -0400] "POST /wp-admin/options.php HTTP/1.1" 500 2958
Tanto el archivo php-errors.log como el archivo debug.log cuando WP_DEBUG es verdadero están vacíos.
La Clase del Plugin (/{plugin-name}-class.php)
namespace wp_plugin_name;
class Plugin_Name
{
const VERSION = '1.1.2';
const TABLE_VERSION = 1;
const CHECK_UPDATE_DEFAULT = 1;
const UPDATE_PERIOD_DEFAULT = 604800;
protected $plugin_slug = 'pluginname-widget';
protected $friendly_name = 'PluginName Widget';
protected static $instance = null;
private function __construct()
{
// Cargar dominio de texto del plugin
add_action( 'init',
array(
$this,
'load_plugin_textdomain' ) );
// Activar plugin cuando se agrega un nuevo blog
add_action( 'wpmu_new_blog',
array(
$this,
'activate_new_site' ) );
// Cargar hoja de estilos y JavaScript de cara al público
add_action( 'wp_enqueue_scripts',
array(
$this,
'enqueue_styles' ) );
add_action( 'wp_enqueue_scripts',
array(
$this,
'enqueue_scripts' ) );
/* Definir funcionalidad personalizada.
* Consultar http://codex.wordpress.org/Plugin_API#Hooks.2C_Actions_and_Filters
*/
}
public function get_plugin_slug()
{
return $this->plugin_slug;
}
public function get_name()
{
return $this->friendly_name;
}
public static function get_instance()
{
// Si la instancia única no se ha establecido, establecerla ahora.
if ( null == self::$instance )
{
self::$instance = new self;
}
return self::$instance;
}
/**
* Las funciones miembro activate(), deactivate(), y update() son muy similares.
* Ver el plugin Boilerplate para más detalles...
*/
private static function single_activate()
{
if ( !current_user_can( 'activate_plugins' ) )
return;
$plugin_request = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
check_admin_referer( "activate-plugin_$plugin_request" );
/**
* Probar si esta es una instalación nueva
*/
if ( get_option( 'plugin-name_version' ) === false )
{
// Obtener el tiempo como marca de tiempo Unix y agregar una semana
$unix_time_utc = time() + Plugin_Name::UPDATE_PERIOD_DEFAULT;
add_option( 'plugin-name_version', Plugin_Name::VERSION );
add_option( 'plugin-name_check_updates',
Plugin_Name::CHECK_UPDATE_DEFAULT );
add_option( 'plugin-name_update_frequency',
Plugin_Name::UPDATE_PERIOD_DEFAULT );
add_option( 'plugin-name_next_check', $unix_time_utc );
// Crear tabla de opciones
table_update();
// Informar al usuario que PluginName se instaló correctamente
is_admin() && add_filter( 'gettext', 'finalization_message', 99, 3 );
}
else
{
// Informar al usuario que PluginName se activó correctamente
is_admin() && add_filter( 'gettext', 'activate_message', 99, 3 );
}
}
private static function single_update()
{
if ( !current_user_can( 'activate_plugins' ) )
return;
$plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
check_admin_referer( "activate-plugin_{$plugin}" );
$cache_plugin_version = get_option( 'plugin-name_version' );
$cache_table_version = get_option( 'plugin-name_table_version' );
$cache_deferred_admin_notices = get_option( 'plugin-name_admin_messages',
array() );
/**
* Averiguar qué versión de nuestro plugin estamos ejecutando y compararla con nuestra
* versión definida aquí
*/
if ( $cache_plugin_version > self::VERSION )
{
$cache_deferred_admin_notices[] = array(
'error',
"Parece que está intentando revertir a una versión anterior de " . $this->get_name() . ". La reversión a través de la función de actualización no está soportada."
);
}
else if ( $cache_plugin_version === self::VERSION )
{
$cache_deferred_admin_notices[] = array(
'updated',
"¡Ya estás usando la última versión de " . $this->get_name() . "!"
);
return;
}
/**
* Si no podemos determinar en qué versión está la tabla, actualizarla...
*/
if ( !is_int( $cache_table_version ) )
{
update_option( 'plugin-name_table_version', TABLE_VERSION );
table_update();
}
/**
* De lo contrario, solo verificaremos si se necesita una actualización
*/
else if ( $cache_table_version < TABLE_VERSION )
{
table_update();
}
/**
* La tabla no necesitaba actualización.
* Ten en cuenta que no podemos actualizar ninguna otra opción porque no podemos asumir que siguen siendo
* los valores predeterminados para nuestro plugin... (a menos que los hayamos almacenado en la bd)
*/
}
private static function single_deactivate()
{
// Determinar si el usuario actual tiene los permisos adecuados
if ( !current_user_can( 'activate_plugins' ) )
return;
// ¿Hay algún dato de solicitud?
$plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
// Verificar si el nonce era válido
check_admin_referer( "deactivate-plugin_{$plugin}" );
// Bueno, técnicamente el plugin no está incluido cuando está desactivado así que...
// No hacer nada
}
public function load_plugin_textdomain()
{
$domain = $this->plugin_slug;
$locale = apply_filters( 'plugin_locale', get_locale(), $domain );
load_textdomain( $domain,
trailingslashit( WP_LANG_DIR ) . $domain . '/' . $domain . '-' . $locale . '.mo' );
load_plugin_textdomain( $domain, FALSE,
basename( plugin_dir_path( dirname( __FILE__ ) ) ) . '/languages/' );
}
public function activate_message( $translated_text, $untranslated_text,
$domain )
{
$old = "Plugin <strong>activated</strong>.";
$new = FRIENDLY_NAME . " fue <strong>activado exitosamente</strong> ";
if ( $untranslated_text === $old )
$translated_text = $new;
return $translated_text;
}
public function finalization_message( $translated_text, $untranslated_text,
$domain )
{
$old = "Plugin <strong>activated</strong>.";
$new = "Capitán, el Núcleo está estable y PluginName fue <strong>instalado exitosamente</strong> y listo para velocidad Warp";
if ( $untranslated_text === $old )
$translated_text = $new;
return $translated_text;
}
}
Referencias:

Error: "Página de opciones no encontrada"
Este es un problema conocido en la API de Configuración de WP. Hubo un ticket abierto hace años que fue marcado como resuelto, pero el error persiste en las últimas versiones de WordPress. Esto es lo que la página del Codex (ahora eliminada) decía al respecto:
El problema "Error: página de opciones no encontrada" (incluyendo una solución y explicación):
El problema es que el filtro 'whitelist_options' no tiene el índice correcto para tus datos. Se aplica en options.php#98 (WP 3.4).
register_settings()
añade tus datos al global$new_whitelist_options
. Esto luego se combina con el global$whitelist_options
dentro de los callbacksoption_update_filter()
(resp.add_option_whitelist()
). Esos callbacks añaden tus datos al global$new_whitelist_options
usando$option_group
como índice. Cuando ves "Error: página de opciones no encontrada", significa que tu índice no ha sido reconocido. Lo confuso es que el primer argumento se usa como índice y se llama$options_group
, cuando la verificación real en options.php#112 ocurre contra$options_page
, que es el$hook_suffix
que obtienes como valor @return deadd_submenu_page()
.En resumen, una solución fácil es hacer que
$option_group
coincida con$option_name
. Otra causa de este error es tener un valor inválido para el parámetro$page
al llamar aadd_settings_section( $id, $title, $callback, $page )
oadd_settings_field( $id, $title, $callback, $page, $section, $args )
.Pista:
$page
debe coincidir con$menu_slug
de Function Reference/add theme page.
Solución Simple
Usar el nombre de página personalizado (en tu caso: $this->plugin_slug
) como tu ID de sección evitaría el problema. Sin embargo, todas tus opciones tendrían que estar contenidas en una sola sección.
Solución Completa
Para una solución más robusta, haz estos cambios en tu clase Plugin_Name_Admin
:
Añade al constructor:
// Rastrea nuevas secciones para whitelist_custom_options_page()
$this->page_sections = array();
// Debe ejecutarse después del `option_update_filter()` de WP, por lo que prioridad > 10
add_action( 'whitelist_options', array( $this, 'whitelist_custom_options_page' ),11 );
Añade estos métodos:
// Permite opciones en páginas personalizadas.
// Solución alternativa para el segundo problema: http://j.mp/Pk3UCF
public function whitelist_custom_options_page( $whitelist_options ){
// Las opciones personalizadas se mapean por ID de sección; Re-mapea por slug de página.
foreach($this->page_sections as $page => $sections ){
$whitelist_options[$page] = array();
foreach( $sections as $section )
if( !empty( $whitelist_options[$section] ) )
foreach( $whitelist_options[$section] as $option )
$whitelist_options[$page][] = $option;
}
return $whitelist_options;
}
// Wrapper para `add_settings_section()` de WP que rastrea secciones personalizadas
private function add_settings_section( $id, $title, $cb, $page ){
add_settings_section( $id, $title, $cb, $page );
if( $id != $page ){
if( !isset($this->page_sections[$page]))
$this->page_sections[$page] = array();
$this->page_sections[$page][$id] = $id;
}
}
Y cambia las llamadas a add_settings_section()
por: $this->add_settings_section()
.
Otras notas sobre tu código
- Tu código de formulario es correcto. Tu formulario debe enviarse a options.php, como me señaló @Chris_O y como indica la documentación de la API de Configuración de WP.
- Los namespaces tienen ventajas, pero pueden hacer más compleja la depuración y reducen la compatibilidad de tu código (requiere PHP>=5.3, otros plugins/plantillas que usen autoloaders, etc). Si no hay una buena razón para usar namespaces en tu archivo, no lo hagas. Ya estás evitando conflictos de nombres al envolver tu código en una clase. Haz que los nombres de tus clases sean más específicos y lleva tus callbacks
validate()
a la clase como métodos públicos. - Comparando el boilerplate de plugin que citas con tu código, parece que tu código está basado en un fork o una versión antigua del boilerplate. Incluso los nombres de archivos y rutas son diferentes. Podrías migrar tu plugin a la última versión, pero ten en cuenta que este boilerplate puede no ser adecuado para tus necesidades. Hace uso de singletons, que generalmente están desaconsejados. Hay casos donde el patrón singleton es sensato, pero esto debería ser una decisión consciente, no la solución por defecto.

Es bueno saber que hay un error en la API. Siempre intento revisar el código que escribo en busca de errores que pueda haber introducido. Por supuesto, eso asume que sé un par de cosas.

Para cualquiera que se encuentre con este problema: echen un vistazo al ejemplo de OOP en el codex: https://codex.wordpress.org/Creating_Options_Pages#Example_.232

Acabo de encontrar esta publicación mientras buscaba el mismo problema. La solución es mucho más simple de lo que parece porque la documentación es engañosa: en register_setting() el primer argumento llamado $option_group
es el slug de tu página, no la sección donde quieres mostrar el ajuste.
En el código anterior deberías usar:
// Actualizar Ajustes
add_settings_section(
'maintenance', // slug de la sección
'Mantenimiento', // título de la sección
array( $this, 'maintenance_section' ), // callback para mostrar la sección
$this->plugin_slug // slug de la página
);
// Opción para Verificar Actualizaciones
register_setting(
$this->plugin_slug, // slug de la página, no el slug de la sección
'plugin-name_check_updates', // slug del ajuste
'wp_plugin_name\validate_bool' // inválido, debería ser un array de opciones, ver documentación para más info
);
add_settings_field(
'plugin-name_check_updates', // slug del ajuste
'¿Debería ' . $this->friendly_name . ' verificar actualizaciones?', // título del ajuste
array( $this, 'check_updates_field' ), // callback para mostrar el ajuste
$this->plugin_slug, // slug de la página
'maintenance' // slug de la sección
);

Esto no es correcto. Por favor revisa este ejemplo funcional (no es mío) - https://gist.github.com/annalinneajohansson/5290405

Al registrar una página de opciones con:
add_submenu_page( string $parent_slug, string $page_title, string $menu_title, string $capability, string $menu_slug, callable $function = '' )
Y al registrar configuraciones con:
register_setting( string $option_group, string $option_name );
$option_group
debe ser igual a $menu_slug

Tuve el mismo error pero lo solucioné de una manera diferente:
// no hay código real
// esto falló
add_settings_field('id','título', /*callback*/ function($argumentos) {
// echo $htmlcode;
register_setting('grupo_opcion', 'nombre_opcion');
}), 'página', 'sección');
No sé por qué sucedió esto, pero parece que register_setting
no debería estar en el callback de add_settings_field
// no hay código real
// esto funcionó
add_settings_field('id','título', /*callback*/ function($argumentos) {echo $htmlcode;}), 'página', 'sección');
register_setting('grupo_opcion', 'nombre_opcion');
Espero que esto ayude
