Практическая реализация меню WordPress — от кода к результату
В предыдущих статьях цикла мы разобрали теорию WordPress Settings API и систему меню административной панели. Теперь настало время применить эти знания на практике — собрать работающую страницу опций с меню, секциями, полями и санитизацией. Эта статья целиком посвящена практическому подходу: мы возьмём код из предыдущих уроков, очистим его от мусора и построим полноценную панель настроек темы.
Если вы следовали примерам из прошлых статей, в вашем файле functions.php темы-песочницы скопилось множество функций — меню верхнего уровня, подменю, страницы плагинов, тестовые колбэки. Часть этого кода была написана для демонстрации и больше не нужна. Перед тем как двигаться дальше, проведём ревизию и удалим всё лишнее.
Ревизия кода: что оставить, что удалить
Загляните в ваш functions.php. Вы увидите функции, которые вы добавляли в процессе изучения меню: создание пункта в меню «Плагины» через add_plugins_page, создание меню верхнего уровня через add_menu_page, тестовые подменю, разрозненные колбэки. Весь этот код был учебным — теперь он нам мешает.
Удалите следующие блоки:
- add_plugins_page — страница в меню «Плагины» нам больше не нужна, опции темы должны быть в меню «Внешний вид» или в собственном разделе
- add_menu_page — меню верхнего уровня из демонстрационного примера будет переписано с правильными slug и структурой
- Все колбэки, которые просто выводят тестовый текст вроде «Hello World» или «This is a test page»
- Дублирующиеся вызовы register_setting, если вы экспериментировали с разными группами опций
После очистки в вашем functions.php должны остаться:
- Функция регистрации настроек sandbox_register_settings() с хуком admin_init
- Функция sandbox_sanitize_options() для санитизации массива опций
- Колбэки полей: sandbox_text_field_callback(), sandbox_checkbox_field_callback(), sandbox_textarea_field_callback()
- Колбэк описания секции sandbox_general_section_callback()
Всё остальное удаляем без сожаления. Теперь у нас чистый лист для построения правильной архитектуры.
Планирование структуры опций
Прежде чем писать код, спланируем, какие настройки будут у нашей темы и как они будут организованы. Хаотичное добавление полей приводит к нечитаемой странице опций, которую пользователи будут обходить стороной.
На данный момент у нас есть базовый функционал — пользователь может управлять видимостью трёх основных контейнеров темы (шапка, боковая панель, подвал). Эти опции логично разместить на странице «Настройки темы». План структуры:
| Группа опций | Страница | Поля | Тип полей |
|---|---|---|---|
| Отображение (Display) | Настройки темы | Видимость шапки, видимость сайдбара, видимость футера | Чекбоксы (вкл/выкл) |
| Социальные сети (Social) | Настройки темы | URL Facebook, URL Twitter, URL Instagram | Текстовые поля (валидация URL) |
| Общие (General) | Настройки темы | Название сайта, URL логотипа, текст футера | Текстовые поля + textarea |
Каждая группа будет отдельной секцией (section) в терминах Settings API. Мы создадим меню в разделе «Внешний вид» через add_theme_page() — это стандартный подход для тем WordPress, и пользователи ожидают найти настройки темы именно там.
Регистрация опций для всех трёх групп
Теперь зарегистрируем настройки и создадим секции с полями. Вместо того чтобы использовать три отдельные опции (по одной на группу), объединим всё в один массив sandbox_theme_options — это упростит санитизацию и сократит количество запросов к базе данных:
function sandbox_register_settings() {
register_setting(
'sandbox_options_group',
'sandbox_theme_options',
array(
'type' => 'array',
'sanitize_callback' => 'sandbox_sanitize_options',
'default' => array(
'show_header' => 1,
'show_sidebar' => 1,
'show_footer' => 1,
'facebook_url' => '',
'twitter_url' => '',
'instagram_url' => '',
'site_title' => '',
'logo_url' => '',
'footer_text' => '',
),
)
);
add_settings_section(
'sandbox_display_section',
'Display Options',
'sandbox_display_section_callback',
'sandbox_theme_options_page'
);
add_settings_field(
'sandbox_show_header',
'Show Header',
'sandbox_checkbox_field_callback',
'sandbox_theme_options_page',
'sandbox_display_section',
array( 'label_for' => 'sandbox_show_header', 'option' => 'sandbox_theme_options', 'key' => 'show_header' )
);
add_settings_field(
'sandbox_show_sidebar',
'Show Sidebar',
'sandbox_checkbox_field_callback',
'sandbox_theme_options_page',
'sandbox_display_section',
array( 'label_for' => 'sandbox_show_sidebar', 'option' => 'sandbox_theme_options', 'key' => 'show_sidebar' )
);
add_settings_field(
'sandbox_show_footer',
'Show Footer',
'sandbox_checkbox_field_callback',
'sandbox_theme_options_page',
'sandbox_display_section',
array( 'label_for' => 'sandbox_show_footer', 'option' => 'sandbox_theme_options', 'key' => 'show_footer' )
);
add_settings_section(
'sandbox_social_section',
'Social Network Links',
'sandbox_social_section_callback',
'sandbox_theme_options_page'
);
$social_fields = array(
'facebook_url' => 'Facebook URL',
'twitter_url' => 'Twitter URL',
'instagram_url' => 'Instagram URL',
);
foreach ( $social_fields as $key => $label ) {
add_settings_field(
'sandbox_' . $key,
$label,
'sandbox_url_field_callback',
'sandbox_theme_options_page',
'sandbox_social_section',
array( 'label_for' => 'sandbox_' . $key, 'option' => 'sandbox_theme_options', 'key' => $key )
);
}
add_settings_section(
'sandbox_general_section',
'General Settings',
'sandbox_general_section_callback',
'sandbox_theme_options_page'
);
add_settings_field(
'sandbox_site_title',
'Site Title',
'sandbox_text_field_callback',
'sandbox_theme_options_page',
'sandbox_general_section',
array( 'label_for' => 'sandbox_site_title', 'option' => 'sandbox_theme_options', 'key' => 'site_title' )
);
add_settings_field(
'sandbox_logo_url',
'Logo URL',
'sandbox_url_field_callback',
'sandbox_theme_options_page',
'sandbox_general_section',
array( 'label_for' => 'sandbox_logo_url', 'option' => 'sandbox_theme_options', 'key' => 'logo_url' )
);
add_settings_field(
'sandbox_footer_text',
'Footer Text',
'sandbox_textarea_field_callback',
'sandbox_theme_options_page',
'sandbox_general_section',
array( 'label_for' => 'sandbox_footer_text', 'option' => 'sandbox_theme_options', 'key' => 'footer_text' )
);
}
add_action( 'admin_init', 'sandbox_register_settings' );[/codeblock]Колбэки секций и полей
Определим колбэки для трёх секций — каждая выводит пояснительный текст:
function sandbox_display_section_callback() {
echo 'Choose which containers are visible on your site. Unchecking a container hides it completely from all pages.
'; } function sandbox_social_section_callback() { echo 'Enter the full URLs to your social media profiles. Leave a field empty to hide the corresponding icon.
'; } function sandbox_general_section_callback() { echo 'Basic site identity settings. The Site Title overrides the default WordPress site name in the theme header.
'; }[/codeblock]И отдельный колбэк для URL-полей — он использует type="url" для встроенной валидации браузера:
function sandbox_url_field_callback( $args ) {
$options = get_option( $args['option'], array() );
$value = isset( $options[ $args['key'] ] ) ? $options[ $args['key'] ] : '';
?>
Санитизация с учётом разных типов данныхНаша функция санитизации должна обрабатывать три разных типа данных: чекбоксы (1/0), URL-адреса и текстовые поля. Каждое поле требует своего санитайзера:
function sandbox_sanitize_options( $input ) {
$sanitized = array();
$checkbox_fields = array( 'show_header', 'show_sidebar', 'show_footer' );
foreach ( $checkbox_fields as $field ) {
$sanitized[ $field ] = isset( $input[ $field ] ) && $input[ $field ] ? 1 : 0;
}
$url_fields = array( 'facebook_url', 'twitter_url', 'instagram_url', 'logo_url' );
foreach ( $url_fields as $field ) {
if ( isset( $input[ $field ] ) && ! empty( $input[ $field ] ) ) {
$sanitized[ $field ] = esc_url_raw( $input[ $field ] );
} else {
$sanitized[ $field ] = '';
}
}
$text_fields = array( 'site_title' );
foreach ( $text_fields as $field ) {
$sanitized[ $field ] = isset( $input[ $field ] ) ? sanitize_text_field( $input[ $field ] ) : '';
}
if ( isset( $input['footer_text'] ) ) {
$sanitized['footer_text'] = sanitize_textarea_field( $input['footer_text'] );
}
return $sanitized;
}[/codeblock]Группировка полей по типу данных делает санитизацию предсказуемой. Добавление нового чекбокса сводится к добавлению ключа в массив $checkbox_fields — функция санитизации автоматически подхватит его.
Рендеринг страницы опций через settings_fields() и do_settings_sections()
Теперь создадим страницу, на которой будут отображаться все зарегистрированные секции и поля. Для этого используются две ключевые функции Settings API: settings_fields() выводит скрытые поля (nonce, option_group) для защиты формы, а do_settings_sections() рендерит все секции и поля, привязанные к указанной странице:
function sandbox_theme_options_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
current_user_can( 'manage_options' )— страница доступна только администраторам. Без этой проверки любой авторизованный пользователь, угадавший URL, увидит страницу (но не сможет сохранить настройки из-за проверок в Settings API)settings_fields( 'sandbox_options_group' )— выводит скрытые поля: nonce для защиты от CSRF, action=update, идентификатор option_group. Без этого вызова форма не пройдёт проверку при сохраненииdo_settings_sections( 'sandbox_theme_options_page' )— рендерит все секции и поля, зарегистрированные для страницы sandbox_theme_options_page. Порядок рендеринга соответствует порядку вызовов add_settings_section()submit_button()— стандартная кнопка сохранения WordPress с правильными классами и текстом
Добавление меню через add_theme_page()
Осталось добавить пункт меню, чтобы пользователь мог попасть на страницу опций. Используем add_theme_page() — страница появится в меню «Внешний вид»:
function sandbox_add_theme_options_menu() {
add_theme_page(
'Sandbox Theme Options',
'Theme Options',
'manage_options',
'sandbox-theme-options',
'sandbox_theme_options_page_html'
);
}
add_action( 'admin_menu', 'sandbox_add_theme_options_menu' );[/codeblock]Теперь зайдите в админку: Appearance → Theme Options. Вы увидите страницу с тремя секциями и всеми полями, которые мы зарегистрировали. Заполните поля, нажмите Save — и данные сохранятся в wp_options.
Полный код functions.php на этом этапе смотрите ниже — он включает регистрацию опций, колбэки полей, санитизацию, страницу опций и меню. Скопируйте его в свой файл и проверьте результат.
Тестирование сохранения опций
После сохранения настроек важно убедиться, что данные действительно попали в базу и правильно оттуда извлекаются. WordPress хранит опцию sandbox_theme_options как сериализованный массив в wp_options. Проверить содержимое можно несколькими способами.
Способ 1: через phpMyAdmin. Откройте таблицу wp_options, найдите строку с option_name = 'sandbox_theme_options' и просмотрите значение option_value. Вы увидите сериализованный PHP-массив с ключами show_header, facebook_url, site_title и значениями, которые вы ввели в форму.
Способ 2: через временный отладочный код. Добавьте в footer.php темы:
[/codeblock]После этого откройте любую страницу сайта как администратор и загляните в HTML-код. В футере будет закомментированный дамп массива настроек.
Способ 3: через WP-CLI. Если у вас есть доступ к командной строке сервера: wp option get sandbox_theme_options --format=json выведет значение опции в формате JSON.
Проверка санитизации: как убедиться, что данные чистые
Санитизация работает невидимо — пользователь вводит данные, нажимает «Сохранить», и WordPress пропускает их через sanitize_callback перед записью в базу. Чтобы убедиться, что санитайзер действительно работает, проведите тест с заведомо грязными данными:
- В поле Site Title введите:
<script>alert("XSS")</script>Test - В поле Facebook URL введите:
javascript:alert(1) - Нажмите «Сохранить»
- Проверьте значения в базе — Site Title должен содержать только «Test», Facebook URL должен быть пустой строкой
Если sanitize_text_field удалил скрипт, а esc_url_raw отфильтровал опасный URL — санитизация работает правильно. Если в базе оказался скрипт — вы где-то забыли вызвать санитайзер для конкретного поля.
Полный код functions.php
Ниже приведён полный код functions.php темы-песочницы на данном этапе. Он включает регистрацию всех трёх групп опций, санитизацию, страницу опций через settings_fields() и do_settings_sections(), а также меню через add_theme_page():
'array',
'sanitize_callback' => 'sandbox_sanitize_options',
'default' => array(
'show_header' => 1,
'show_sidebar' => 1,
'show_footer' => 1,
'facebook_url' => '',
'twitter_url' => '',
'instagram_url' => '',
'site_title' => '',
'logo_url' => '',
'footer_text' => '',
),
)
);
add_settings_section(
'sandbox_display_section',
'Display Options',
'sandbox_display_section_callback',
'sandbox_theme_options_page'
);
$display_fields = array( 'show_header' => 'Show Header', 'show_sidebar' => 'Show Sidebar', 'show_footer' => 'Show Footer' );
foreach ( $display_fields as $key => $label ) {
add_settings_field(
'sandbox_' . $key, $label, 'sandbox_checkbox_field_callback',
'sandbox_theme_options_page', 'sandbox_display_section',
array( 'label_for' => 'sandbox_' . $key, 'option' => 'sandbox_theme_options', 'key' => $key )
);
}
add_settings_section(
'sandbox_social_section',
'Social Network Links',
'sandbox_social_section_callback',
'sandbox_theme_options_page'
);
$social_fields = array( 'facebook_url' => 'Facebook URL', 'twitter_url' => 'Twitter URL', 'instagram_url' => 'Instagram URL' );
foreach ( $social_fields as $key => $label ) {
add_settings_field(
'sandbox_' . $key, $label, 'sandbox_url_field_callback',
'sandbox_theme_options_page', 'sandbox_social_section',
array( 'label_for' => 'sandbox_' . $key, 'option' => 'sandbox_theme_options', 'key' => $key )
);
}
add_settings_section(
'sandbox_general_section',
'General Settings',
'sandbox_general_section_callback',
'sandbox_theme_options_page'
);
add_settings_field( 'sandbox_site_title', 'Site Title', 'sandbox_text_field_callback',
'sandbox_theme_options_page', 'sandbox_general_section',
array( 'label_for' => 'sandbox_site_title', 'option' => 'sandbox_theme_options', 'key' => 'site_title' )
);
add_settings_field( 'sandbox_logo_url', 'Logo URL', 'sandbox_url_field_callback',
'sandbox_theme_options_page', 'sandbox_general_section',
array( 'label_for' => 'sandbox_logo_url', 'option' => 'sandbox_theme_options', 'key' => 'logo_url' )
);
add_settings_field( 'sandbox_footer_text', 'Footer Text', 'sandbox_textarea_field_callback',
'sandbox_theme_options_page', 'sandbox_general_section',
array( 'label_for' => 'sandbox_footer_text', 'option' => 'sandbox_theme_options', 'key' => 'footer_text' )
);
}
add_action( 'admin_init', 'sandbox_register_settings' );
function sandbox_display_section_callback() {
echo 'Choose which containers are visible on your site.
'; } function sandbox_social_section_callback() { echo 'Enter the full URLs to your social media profiles.
'; } function sandbox_general_section_callback() { echo 'Basic site identity settings.
'; } function sandbox_text_field_callback( $args ) { $options = get_option( $args['option'], array() ); $value = isset( $options[ $args['key'] ] ) ? $options[ $args['key'] ] : ''; echo ''; } function sandbox_url_field_callback( $args ) { $options = get_option( $args['option'], array() ); $value = isset( $options[ $args['key'] ] ) ? $options[ $args['key'] ] : ''; echo ''; } function sandbox_checkbox_field_callback( $args ) { $options = get_option( $args['option'], array() ); $checked = isset( $options[ $args['key'] ] ) ? (bool) $options[ $args['key'] ] : false; echo ''; echo ' '; } function sandbox_textarea_field_callback( $args ) { $options = get_option( $args['option'], array() ); $value = isset( $options[ $args['key'] ] ) ? $options[ $args['key'] ] : ''; echo ''; } function sandbox_sanitize_options( $input ) { $sanitized = array(); $checkbox_fields = array( 'show_header', 'show_sidebar', 'show_footer' ); foreach ( $checkbox_fields as $field ) { $sanitized[ $field ] = isset( $input[ $field ] ) && $input[ $field ] ? 1 : 0; } $url_fields = array( 'facebook_url', 'twitter_url', 'instagram_url', 'logo_url' ); foreach ( $url_fields as $field ) { $sanitized[ $field ] = isset( $input[ $field ] ) && ! empty( $input[ $field ] ) ? esc_url_raw( $input[ $field ] ) : ''; } $sanitized['site_title'] = isset( $input['site_title'] ) ? sanitize_text_field( $input['site_title'] ) : ''; $sanitized['footer_text'] = isset( $input['footer_text'] ) ? sanitize_textarea_field( $input['footer_text'] ) : ''; return $sanitized; } function sandbox_theme_options_page_html() { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>Хороший код — это не тот, который работает. Хороший код — это тот, который работает, и другой разработчик понимает его за пять минут, а не за час. Группировка полей по типу, циклы вместо копипасты, самодокументированные имена функций — именно это отличает профессиональный код от учебного.
Часто задаваемые вопросы
Зачем удалять старый код перед написанием нового?
Старый учебный код содержит демонстрационные функции, которые конфликтуют с новой архитектурой. Оставленный add_plugins_page будет создавать дублирующее меню. Оставленные тестовые колбэки занимают место и сбивают с толку. Чистый functions.php — основа поддерживаемого проекта.
Почему все опции хранятся в одном массиве, а не по отдельности?
Один массив на все настройки темы означает один вызов get_option() при загрузке страницы вместо трёх. Санитизация тоже в одной функции, что упрощает поддержку. WordPress автоматически загружает опции с autoload=yes при старте, так что разница в производительности минимальна, но архитектурно массив чище.
Что делает settings_fields()?
settings_fields() выводит три скрытых поля формы: nonce для защиты от CSRF-атак, action=update для указания WordPress, что это форма сохранения настроек, и option_group для связи формы с зарегистрированной группой опций. Без этого вызова форма не сохранит данные.
Что делает do_settings_sections()?
do_settings_sections() рендерит все секции и поля, зарегистрированные для указанной страницы (page slug). Порядок вывода соответствует порядку вызовов add_settings_section(). Для каждой секции выводится заголовок, описание и все привязанные поля.
Обязательно ли проверять current_user_can в колбэке страницы?
Да. WordPress проверяет capability при показе пункта меню, но не при прямом переходе по URL. Пользователь, угадавший admin.php?page=sandbox-theme-options, увидит страницу. current_user_can() в начале колбэка блокирует несанкционированный доступ.
Чем отличается сохранение массива опций от одиночных полей?
При сохранении массива все поля обновляются одновременно одним вызовом update_option(). При одиночных полях каждое поле — отдельный вызов. Сравним подходы:
| Характеристика | Массив (один ключ) | Одиночные опции |
|---|---|---|
| Атомарность | Все поля сохраняются или ни одного | Поля могут сохраниться частично |
| Количество запросов | 1 запрос на сохранение | N запросов по числу полей |
| Откат при ошибке | Легко — возвращаем старый массив | Сложно — нужно отслеживать каждое поле |
| Удобство отладки | Один дамп показывает всё | Нужно проверять N строк wp_options |
Добавьте ключ поля в массив $text_fields внутри sandbox_sanitize_options() и вызовите add_settings_field() с новым ключом внутри sandbox_register_settings(). Если новая секция не требуется, поле появится на странице автоматически после добавления одного вызова add_settings_field().
Как убедиться, что санитизация работает для URL-полей?
Введите javascript:alert(1) в поле Facebook URL, сохраните настройки и проверьте значение опции через get_option(). Корректно работающий esc_url_raw() вернёт пустую строку. Если вернулся javascript:alert(1) — санитайзер не отработал или не добрался до этого поля.
Почему используется add_theme_page(), а не add_menu_page()?
add_theme_page() помещает настройки в меню «Внешний вид» — стандартное место для опций темы. Пользователи ожидают найти настройки темы именно там. add_menu_page() создаёт отдельный пункт верхнего уровня, что оправдано для крупных плагинов, но для темы избыточно.
Что будет, если option_group в settings_fields() не совпадает с register_setting()?
Форма не сохранит данные. WordPress сверит option_group из запроса с зарегистрированными настройками, не найдёт совпадений и молча отклонит запрос. Никаких ошибок в интерфейсе не появится — это самая сложная для отладки ошибка Settings API.
Можно ли объединить настройки нескольких плагинов в одну страницу?
Да, но каждый плагин должен регистрировать свою группу опций и вызывать settings_fields() с разными параметрами. На одной странице не может быть двух вызовов settings_fields() — форма поддерживает только одну группу опций за раз. Используйте вкладки (tabs) для разделения групп на одной странице.
Нажмите для реакции



