Пример Settings API с массивами

20 мая 2013 г., 03:51:31
Просмотры: 26.1K
Голосов: 40

Я использую книгу Wrox по разработке плагинов WordPress в качестве основного руководства для начала работы с новым плагином и понимаю, что все настройки можно сохранить как 1 массив, но в книге нет примера этого, а все материалы, которые я нахожу в интернете, сильно отличаются друг от друга. Вторая половина поста от Константина близка к тому, что мне нужно, но я бы хотел увидеть более полный пример с несколькими полями.

0
Все ответы на вопрос 1
10
43

Краткий ответ: значения атрибута name должны использовать схему option_name[array_key]. Таким образом, когда вы используете …

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

… вы получаете массив в качестве значения опции в вашей функции валидации:

array (
    'key1' => 'some value',
    'key2' => 'some other value'
)

PHP делает это автоматически, это не особенность WordPress. :)

Как заставить это работать с API настроек?

Допустим, мы хотим создать такую страницу настроек, и все значения должны храниться в одной опции и валидироваться одной функцией.

Пример страницы настроек

Страница настроек

Нам нужен хук admin_menu и две функции: одна для регистрации страницы, другая для вывода содержимого.

add_action( 'admin_menu', 't5_sae_add_options_page' );

function t5_sae_add_options_page()
{
    add_options_page(
        'Пример API настроек 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
}

Атрибут формы action должен быть options.php, иначе валидация не будет вызвана. Посмотрите исходный код PHP в wp-admin/options-permalink.php – там скрытая ловушка do_settings_sections('permalink');, но она не работает, потому что атрибут action формы указан неправильно.

Теперь вернемся к нашей кастомной странице. Мы сделаем её лучше, чем у WordPress.

Регистрация настроек, секций и полей

Мы добавляем хук в admin_init когда это нужно и вызываем функцию регистрации.

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

Важная часть здесь: $GLOBALS['pagenow'] должен быть либо options-general.php (для вывода), либо options.php (для валидации). Не вызывайте весь следующий код при каждом запросе. Большинство руководств и почти все плагины делают это неправильно.

Ладно, давайте зарегистрируем всё:

  1. Получаем значения опций для нашей страницы и проверяем их на соответствие значениям по умолчанию. Достаточно просто.

  2. Регистрируем группу настроек с именем plugin:t5_sae_option_group. Мне нравятся имена с префиксами, так их легче сортировать и понимать.

  3. Затем регистрируем две секции, 1 и 2.

  4. Добавляем три поля: два для первой секции, одно для второй. Передаем имя опции и экранированное значение в callback-функции для каждого поля. Обработчики вывода не должны изменять данные, только добавлять HTML.

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

    // Получаем существующие опции.
    $option_values = get_option( $option_name );

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

    // Проверяем значения опций на соответствие ключам, остальное отбрасываем.
    $data = shortcode_atts( $default_values, $option_values );

    register_setting(
        'plugin:t5_sae_option_group', // группа, используется в settings_fields()
        $option_name,  // имя опции, используется как ключ в базе данных
        't5_sae_validate_option'      // callback-функция валидации
    );

    /* Ни один аргумент не связан с предыдущим register_setting(). */
    add_settings_section(
        'section_1', // ID
        'Некоторые текстовые поля', // Заголовок
        't5_sae_render_section_1', // вывод
        't5_sae_slug' // menu slug, см. t5_sae_add_options_page()
    );

    add_settings_field(
        'section_1_field_1',
        'Число',
        't5_sae_render_section_1_field_1',
        't5_sae_slug',  // menu slug, см. t5_sae_add_options_page()
        'section_1',
        array (
            'label_for'   => 'label1', // делает имя поля кликабельным,
            'name'        => 'number', // значение для атрибута 'name'
            'value'       => esc_attr( $data['number'] ),
            'option_name' => $option_name
        )
    );
    add_settings_field(
        'section_1_field_2',
        'Выбор',
        't5_sae_render_section_1_field_2',
        't5_sae_slug',  // menu slug, см. t5_sae_add_options_page()
        'section_1',
        array (
            'label_for'   => 'label2', // делает имя поля кликабельным,
            'name'        => 'color', // значение для атрибута 'name'
            'value'       => esc_attr( $data['color'] ),
            'options'     => array (
                'blue'  => 'Синий',
                'red'   => 'Красный',
                'black' => 'Черный'
            ),
            'option_name' => $option_name
        )
    );

    add_settings_section(
        'section_2', // ID
        'Текстовое поле', // Заголовок
        't5_sae_render_section_2', // вывод
        't5_sae_slug' // menu slug, см. t5_sae_add_options_page()
    );

    add_settings_field(
        'section_2_field_1',
        'Заметки',
        't5_sae_render_section_2_field_1',
        't5_sae_slug',  // menu slug, см. t5_sae_add_options_page()
        'section_2',
        array (
            'label_for'   => 'label3', // делает имя поля кликабельным,
            'name'        => 'long', // значение для атрибута 'name'
            'value'       => esc_textarea( $data['long'] ),
            'option_name' => $option_name
        )
    );
}

Все эти callback-обработчики для секций и полей будут вызваны автоматически, когда мы вызываем do_settings_sections( 't5_sae_slug' ); на нашей странице. Мы уже сделали это, поэтому нам осталось только…

Вывести поля

Обратите внимание, как формируются атрибуты name: переданное option_name – первая часть, за которой следует ключ массива в квадратных скобках [].

function t5_sae_render_section_1()
{
    print '<p>Выберите число от 1 до 1000 и цвет.</p>';
}
function t5_sae_render_section_1_field_1( $args )
{
    /* Создает такую разметку:
    /* <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>Сделайте заметки.</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']
    );
}

Кстати, я ввел функцию t5_sae_debug_var(). Вот она:

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

Полезно, чтобы убедиться, что мы получаем то, что ожидали.

Теперь всё работает довольно хорошо, нам осталось только одно:

Валидация массива опций

Поскольку мы использовали нотацию с квадратными скобками, наше значение – это массив. Нам просто нужно пройтись по каждому элементу и проверить его.

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

    if ( ! is_array( $values ) ) // какие-то некорректные данные
        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',
                        'Число должно быть от 1 до 1000.'
                    );
                elseif ( 1000 < $values[ $key ] )
                    add_settings_error(
                        'plugin:t5_sae_option_group',
                        'number-too-high',
                        'Число должно быть от 1 до 1000.'
                    );
                else
                    $out[ $key ] = $values[ $key ];
            }
            elseif ( 'long' === $key )
            {
                $out[ $key ] = trim( $values[ $key ] );
            }
            else
            {
                $out[ $key ] = $values[ $key ];
            }
        }
    }

    return $out;
}

Это довольно уродливо; я бы не использовал такой код в продакшене. Но он делает то, что должен: возвращает проверенный массив значений. WordPress сериализует массив, сохранит его в базе данных под нашим именем опции и вернет его десериализованным, когда мы вызовем get_option().


Всё это работает, но это излишне сложно, мы получаем разметку из 1998 года (<tr valign="top">) и множество избыточностей.

Используйте API настроек, когда это необходимо. В качестве альтернативы используйте admin_url( 'admin-post.php' ) в качестве атрибута action формы (посмотрите её исходный код) и создайте полностью свою страницу настроек с более элегантным кодом.

Фактически, вам придется это делать, если вы пишете сетевой плагин, потому что API настроек там не работает.

Есть также некоторые крайние случаи и неполные части, которые я здесь не упомянул – вы найдете их, когда они вам понадобятся. :)

21 мая 2013 г. 02:17:42
Комментарии

Вау, спасибо. Это очень полезно. Ни в одном из других постов, которые я читал, не упоминалось о сетевых плагинах, что является важным замечанием, которое я учту в будущем.

Bjorn Bjorn
21 мая 2013 г. 08:37:58

Небольшое дополнение к этому. Если вы пытаетесь отобразить/сохранить чекбоксы, я изменил код callback на: '<input type="checkbox" id="%3$s" name="%1$s[%2$s] value="%4$s" ' . checked('on', $args['value'], false) . '/>'

joesk joesk
10 апр. 2014 г. 19:25:41

Пересматривая ответ, я озадачен использованием plugin:t5_sae_option_group, где используется одиночное двоеточие. Я тщательно искал, но не нашел объяснения этого синтаксиса. Не могли бы вы указать на объяснение этого в документации PHP? Спасибо.

User User
27 апр. 2014 г. 17:41:47

@user50909 : похоже, это просто строковые идентификаторы. Синтаксис PHP не должен быть фактором.

s_ha_dum s_ha_dum
27 апр. 2014 г. 18:04:13

@user50909 Двоеточие — это просто маркер в строке. Так проще найти все мои опции в базе данных.

fuxia fuxia
27 апр. 2014 г. 18:06:52

В мультисайтах $GLOBALS['pagenow'] всегда кажется пустым. Что мы должны проверять перед подключением к хуку admin_init для регистрации настроек?

Dan Dan
26 мая 2014 г. 16:45:11

@Dan Попробуйте basename( $_SERVER['REQUEST_URI'] ).

fuxia fuxia
26 мая 2014 г. 16:48:48

Спасибо! Строка запроса мешала, поэтому пришлось сначала убрать её, но в остальном идеально.

Dan Dan
26 мая 2014 г. 18:03:08

Отлично, очень хорошо структурировано!

Mike C Mike C
22 февр. 2023 г. 18:09:57

Привет @fuxia, ты упомянула о том, чтобы избегать регистрации настроек на ненужных страницах, а также сказала, что большинство разработчиков делают это неправильно и регистрируют настройки всегда. Можешь пояснить? Избегание выполнения кода, который не требуется на текущей странице, звучит как что-то положительное. Однако не будет ли это также плохой практикой, нарушающей основную функциональность? Функция register_option() добавляет опцию в глобальную переменную WordPress, которую можно получить с помощью get_registered_settings(). Зачем тогда добавлять такую функцию, если мы не должны всегда регистрировать наши настройки? Спасибо :)

Gerard Reches Gerard Reches
9 янв. 2024 г. 04:38:25
Показать остальные 5 комментариев