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

Краткий ответ: значения атрибута 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
(для валидации). Не вызывайте весь следующий код при каждом запросе. Большинство руководств и почти все плагины делают это неправильно.
Ладно, давайте зарегистрируем всё:
Получаем значения опций для нашей страницы и проверяем их на соответствие значениям по умолчанию. Достаточно просто.
Регистрируем группу настроек с именем
plugin:t5_sae_option_group
. Мне нравятся имена с префиксами, так их легче сортировать и понимать.Затем регистрируем две секции, 1 и 2.
Добавляем три поля: два для первой секции, одно для второй. Передаем имя опции и экранированное значение в 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 настроек там не работает.
Есть также некоторые крайние случаи и неполные части, которые я здесь не упомянул – вы найдете их, когда они вам понадобятся. :)

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

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

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

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

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

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

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

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