Ejemplo de Settings API con arrays

20 may 2013, 03:51:31
Vistas: 26.1K
Votos: 40

Estoy usando el libro de desarrollo de plugins de WordPress de Wrox como referencia principal para comenzar con un nuevo plugin y entiendo que todas las configuraciones se pueden guardar como 1 array, pero el libro no proporciona un ejemplo de esto y todo el material que encuentro en la web parece tan diferente de un ejemplo a otro. La segunda mitad de una publicación de Konstantin me acerca a lo que busco, pero me gustaría ver un ejemplo más completo con múltiples campos.

0
Todas las respuestas a la pregunta 1
10
43

Respuesta corta: los valores del atributo name deben usar el esquema option_name[array_key]. Entonces, cuando usas...

<input name="option_name[key1]">
<input name="option_name[key2]">

... obtienes un array como valor de opción en tu función de validación:

array (
    'key1' => 'algún valor',
    'key2' => 'algún otro valor'
)

PHP hace esto por ti, no es una característica de WordPress. :)

¿Cómo hacer que esto funcione con la API de ajustes?

Digamos que queremos esta página de opciones, y todos los valores deben almacenarse en una opción y validarse en una función.

Imagen de la página de opciones

La página de opciones

Necesitamos el hook admin_menu y dos funciones: una para registrar la página, otra para renderizar el output.

add_action( 'admin_menu', 't5_sae_add_options_page' );

function t5_sae_add_options_page()
{
    add_options_page(
        'Ejemplo de API de Ajustes T5', // $page_title,
        'T5 SAE',                      // $menu_title,
        'manage_options',               // $capability,
        't5_sae_slug',                  // $menu_slug
        't5_sae_render_page'            // Callback
    );
}

function t5_sae_render_page()
{
    ?>
    <div class="wrap">
        <h2><?php print $GLOBALS['title']; ?></h2>
        <form action="options.php" method="POST">
            <?php 
            settings_fields( 'plugin:t5_sae_option_group' );
            do_settings_sections( 't5_sae_slug' ); 
            submit_button(); 
            ?>
        </form>
    </div>
    <?php
}

El action del formulario debe ser options.php, o la validación no se llamará. Mira el código fuente PHP de wp-admin/options-permalink.php – hay una trampa oculta do_settings_sections('permalink'); – pero no puede funcionar porque el action del formulario es incorrecto.

Ahora, volvamos a nuestra página personalizada. La hacemos mejor que WordPress.

Registrar ajustes, secciones y campos

Enganchamos en admin_init cuando lo necesitamos y llamamos a una función de registro.

if ( ! empty ( $GLOBALS['pagenow'] )
    and ( 'options-general.php' === $GLOBALS['pagenow']
        or 'options.php' === $GLOBALS['pagenow']
    )
)
{
    add_action( 'admin_init', 't5_sae_register_settings' );
}

La parte importante aquí es: $GLOBALS['pagenow'] debe ser options-general.php (para el output) o options.php (para la validación). No llames a todo este código en cada request. La mayoría de tutoriales y casi todos los plugins hacen esto mal.

Vale, registremos como locos:

  1. Obtenemos los valores de opción para nuestra página y los comparamos con algunos defaults. Bastante básico.

  2. Registramos un grupo de ajustes con el nombre plugin:t5_sae_option_group. Me gustan los nombres con prefijo, son más fáciles de ordenar y entender así.

  3. Luego registramos dos secciones, 1 y 2.

  4. Y añadimos tres campos, dos para la primera sección, uno para la segunda. Pasamos el nombre de opción y el valor escapado a las funciones callback para cada campo. Los manejadores de output no deben cambiar datos, solo añadir HTML.

function t5_sae_register_settings()
{
    $option_name   = 'plugin:t5_sae_option_name';

    // Obtiene opciones existentes.
    $option_values = get_option( $option_name );

    $default_values = array (
        'number' => 500,
        'color'  => 'blue',
        'long'   => ''
    );

    // Parsea valores de opción en claves predefinidas, descarta el resto.
    $data = shortcode_atts( $default_values, $option_values );

    register_setting(
        'plugin:t5_sae_option_group', // grupo, usado para settings_fields()
        $option_name,  // nombre de opción, usado como clave en la base de datos
        't5_sae_validate_option'      // callback de validación
    );

    /* Ningún argumento tiene relación con el register_setting() anterior. */
    add_settings_section(
        'section_1', // ID
        'Algunos campos de texto', // Título
        't5_sae_render_section_1', // imprime output
        't5_sae_slug' // menu slug, ver t5_sae_add_options_page()
    );

    add_settings_field(
        'section_1_field_1',
        'Un Número',
        't5_sae_render_section_1_field_1',
        't5_sae_slug',  // menu slug, ver t5_sae_add_options_page()
        'section_1',
        array (
            'label_for'   => 'label1', // hace el nombre del campo clickable,
            'name'        => 'number', // valor para atributo 'name'
            'value'       => esc_attr( $data['number'] ),
            'option_name' => $option_name
        )
    );
    add_settings_field(
        'section_1_field_2',
        'Seleccionar',
        't5_sae_render_section_1_field_2',
        't5_sae_slug',  // menu slug, ver t5_sae_add_options_page()
        'section_1',
        array (
            'label_for'   => 'label2', // hace el nombre del campo clickable,
            'name'        => 'color', // valor para atributo 'name'
            'value'       => esc_attr( $data['color'] ),
            'options'     => array (
                'blue'  => 'Azul',
                'red'   => 'Rojo',
                'black' => 'Negro'
            ),
            'option_name' => $option_name
        )
    );

    add_settings_section(
        'section_2', // ID
        'Área de texto', // Título
        't5_sae_render_section_2', // imprime output
        't5_sae_slug' // menu slug, ver t5_sae_add_options_page()
    );

    add_settings_field(
        'section_2_field_1',
        'Notas',
        't5_sae_render_section_2_field_1',
        't5_sae_slug',  // menu slug, ver t5_sae_add_options_page()
        'section_2',
        array (
            'label_for'   => 'label3', // hace el nombre del campo clickable,
            'name'        => 'long', // valor para atributo 'name'
            'value'       => esc_textarea( $data['long'] ),
            'option_name' => $option_name
        )
    );
}

Todos esos manejadores callback para las secciones y campos serán llamados automáticamente cuando llamamos a do_settings_sections( 't5_sae_slug' ); en nuestra página. Ya lo hicimos, así que solo necesitamos...

Imprimir los campos

Nota cómo se construyen los atributos name: el option_name pasado es la primera parte, la clave del array sigue entre corchetes [].

function t5_sae_render_section_1()
{
    print '<p>Elige un número entre 1 y 1000, y selecciona un color.</p>';
}
function t5_sae_render_section_1_field_1( $args )
{
    /* Crea este markup:
    /* <input name="plugin:t5_sae_option_name[number]"
     */
    printf(
        '<input name="%1$s[%2$s]" id="%3$s" value="%4$s" class="regular-text">',
        $args['option_name'],
        $args['name'],
        $args['label_for'],
        $args['value']
    );
    // t5_sae_debug_var( func_get_args(), __FUNCTION__ );
}
function t5_sae_render_section_1_field_2( $args )
{
    printf(
        '<select name="%1$s[%2$s]" id="%3$s">',
        $args['option_name'],
        $args['name'],
        $args['label_for']
    );

    foreach ( $args['options'] as $val => $title )
        printf(
            '<option value="%1$s" %2$s>%3$s</option>',
            $val,
            selected( $val, $args['value'], FALSE ),
            $title
        );

    print '</select>';

    // t5_sae_debug_var( func_get_args(), __FUNCTION__ );
}
function t5_sae_render_section_2()
{
    print '<p>Toma algunas notas.</p>';
}

function t5_sae_render_section_2_field_1( $args )
{
    printf(
        '<textarea name="%1$s[%2$s]" id="%3$s" rows="10" cols="30" class="code">%4$s</textarea>',
        $args['option_name'],
        $args['name'],
        $args['label_for'],
        $args['value']
    );
}

Ah, he introducido una función t5_sae_debug_var(). Aquí está:

function t5_sae_debug_var( $var, $before = '' )
{
    $export = esc_html( var_export( $var, TRUE ) );
    print "<pre>$before = $export</pre>";
}

Útil para ver si obtuvimos lo esperado.

Ahora, esto funciona bastante bien, solo necesitamos una cosa más:

Validar el array de opciones

Como usamos la notación de corchetes, nuestro valor es un array. Solo tenemos que recorrer cada elemento y validarlo.

function t5_sae_validate_option( $values )
{
    $default_values = array (
        'number' => 500,
        'color'  => 'blue',
        'long'   => ''
    );

    if ( ! is_array( $values ) ) // datos inválidos
        return $default_values;

    $out = array ();

    foreach ( $default_values as $key => $value )
    {
        if ( empty ( $values[ $key ] ) )
        {
            $out[ $key ] = $value;
        }
        else
        {
            if ( 'number' === $key )
            {
                if ( 0 > $values[ $key ] )
                    add_settings_error(
                        'plugin:t5_sae_option_group',
                        'number-too-low',
                        'El número debe estar entre 1 y 1000.'
                    );
                elseif ( 1000 < $values[ $key ] )
                    add_settings_error(
                        'plugin:t5_sae_option_group',
                        'number-too-high',
                        'El número debe estar entre 1 y 1000.'
                    );
                else
                    $out[ $key ] = $values[ $key ];
            }
            elseif ( 'long' === $key )
            {
                $out[ $key ] = trim( $values[ $key ] );
            }
            else
            {
                $out[ $key ] = $values[ $key ];
            }
        }
    }

    return $out;
}

Esto es bastante feo; no usaría tal código en producción. Pero hace lo que debe: devuelve un array de valores validados. WordPress serializará el array, lo almacenará bajo nuestro nombre de opción en la base de datos y lo devolverá deserializado cuando llamemos a get_option().


Todo esto funciona, pero es innecesariamente complicado, obtenemos markup de 1998 (<tr valign="top">), y muchas redundancias.

Usa la API de ajustes cuando debas. Como alternativa usa admin_url( 'admin-post.php' ) como acción del formulario (mira su código fuente) y crea la página de ajustes completa con tu propio código, probablemente más elegante.

De hecho, tienes que hacer esto cuando escribes un plugin para la red, porque la API de ajustes no funciona allí.

También hay algunos casos extremos y partes incompletas que no mencioné aquí – los encontrarás cuando los necesites. :)

21 may 2013 02:17:42
Comentarios

Guau, gracias. Esto es muy útil. Ninguno de los otros posts que leí mencionó nada sobre los plugins de red, lo cual es un detalle importante que tendré en cuenta para el futuro.

Bjorn Bjorn
21 may 2013 08:37:58

Solo un añadido a esto. Si estás intentando mostrar/almacenar casillas de verificación, he modificado el código de callback para que sea: '<input type="checkbox" id="%3$s" name="%1$s[%2$s] value="%4$s" ' . checked('on', $args['value'], false) . '/>'

joesk joesk
10 abr 2014 19:25:41

Revisando la respuesta, me desconcierta el uso de plugin:t5_sae_option_group que incluye un solo dos puntos. He buscado exhaustivamente y no he encontrado una explicación de esta sintaxis. ¿Podrías indicarme dónde encontrar una explicación de esto en la documentación de PHP? Gracias.

User User
27 abr 2014 17:41:47

@user50909 : esos parecen ser simples identificadores de cadena para mí. La sintaxis de PHP no debería ser un factor.

s_ha_dum s_ha_dum
27 abr 2014 18:04:13

@user50909 Los dos puntos son solo un marcador en la cadena. Hace más fácil encontrar todas mis opciones en la base de datos.

fuxia fuxia
27 abr 2014 18:06:52

Para multisitio, $GLOBALS['pagenow'] parece estar siempre vacío. ¿Qué deberíamos verificar antes de enganchar admin_init para registrar configuraciones?

Dan Dan
26 may 2014 16:45:11

@Dan Prueba basename( $_SERVER['REQUEST_URI'] ).

fuxia fuxia
26 may 2014 16:48:48

¡Gracias! La cadena de consulta estaba estropeando las cosas, así que tuve que quitarla antes de que funcionara, pero por lo demás perfecto.

Dan Dan
26 may 2014 18:03:08

¡Esto es genial, muy bien organizado!

Mike C Mike C
22 feb 2023 18:09:57

Hola @fuxia, mencionas evitar registrar la configuración cuando no es necesario en la página actual y también dices que la mayoría de los desarrolladores se equivocan al registrarla siempre. ¿Podrías elaborar? Evitar la ejecución de código que no se requiere en la página actual suena como algo positivo. Sin embargo, ¿no sería eso también una mala práctica que rompe la funcionalidad central? La función register_option() agrega la opción a una variable global de WordPress que puede recuperarse con get_registered_settings(). ¿Por qué agregarían tal función si no se supone que siempre registremos nuestras configuraciones? Gracias :)

Gerard Reches Gerard Reches
9 ene 2024 04:38:25
Mostrar los 5 comentarios restantes