Разработка плагина WordPress с нуля
Соберите плагин WordPress с нуля: хуки, активация и деактивация, страница настроек, санитизация, экранирование и безопасное сохранение.
Плагин WordPress в самом простом виде — это папка внутри wp-content/plugins, содержащая один PHP-файл, который начинается с комментария-заголовка с названием плагина. Сохраните этот файл, и WordPress отобразит плагин на экране «Плагины».
Если вы привыкли добавлять сниппеты в functions.php темы, вам знакома проблема: смените тему — и код исчезнет вместе с ней. Перенос этого кода в плагин означает, что функциональность переживёт смену темы, и это главная причина, по которой поведение сайта должно жить в плагинах, а оформление — в темах.
В этой статье мы полностью, от начала до конца, соберём один небольшой плагин: уведомление на сайте, выводимое в подвале и редактируемое со страницы настроек с безопасным сохранением. Каждый блок кода представляет собой рабочее состояние плагина, так что вы можете остановиться после любого раздела и активировать то, что уже получилось.
Ключевые выводы
- Плагин — это папка в
wp-content/pluginsс одним PHP-файлом, в комментарии-заголовке которого указано имя плагина;Plugin Name— единственное обязательное поле заголовка. - Код плагина ничего не делает, пока вы не привяжете функцию к хуку через
add_action()илиadd_filter(). - Callback-функция фильтра обязана возвращать полученное значение; если ничего не вернуть, фильтруемое значение окажется пустым.
- Безопасная страница настроек строится на четырёх уровнях: проверка прав (capability), nonce, санитизация на входе и экранирование на выходе.
register_activation_hook()предназначен для значений по умолчанию, деактивация — для временной очистки, а безвозвратное удаление данных относится к процедуре удаления (uninstall).
Как создать файл плагина WordPress и его заголовок
Создайте папку wp-content/plugins/site-notice/, а внутри неё — файл site-notice.php. WordPress формирует экран «Плагины», считывая PHP-файлы в папке плагинов и отбирая те, что начинаются с заголовка плагина. Каждый файл с заголовком считается отдельным плагином, поэтому размещайте заголовок только в одном файле.
<?php
/**
* Plugin Name: Site Notice
* Description: Shows a short notice in the site footer, editable from Settings.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: OpenReplay Team
* License: GPL-2.0-or-later
* Text Domain: site-notice
*/
На странице требований к заголовку перечислены все распознаваемые поля, включая Update URI и Requires Plugins. Те, что понадобятся чаще всего:
| Поле | Что WordPress с ним делает | Обязательное |
|---|---|---|
| Plugin Name | Отображает в списке плагинов | Да |
| Description | Показывает под названием | Нет |
| Version | Отображает; используется при сравнении версий для обновлений | Нет |
| Requires at least / Requires PHP | Блокирует активацию в устаревших окружениях | Нет |
| Author, License, Text Domain | Авторство, лицензирование, слаг для перевода | Нет |
Сохраните файл — и плагин появится на экране «Плагины». Он активируется без ошибок и ничего не делает.
Что делают хуки активации и деактивации?
register_activation_hook() выполняет ваш callback один раз, в момент включения плагина, что делает его удобным местом для записи стартовых значений опций в базу данных. register_deactivation_hook() служит для удаления всего, что было нужно плагину только во время работы, —典型ный пример — кеш. Окончательное удаление данных, включая опции и собственные таблицы, — задача процедуры uninstall, поскольку пользователи часто отключают плагин с намерением включить его позже.
Добавьте под заголовком:
function orp_activate() {
add_option( 'orp_notice_text', 'Welcome to the site.' );
}
register_activation_hook( __FILE__, 'orp_activate' );
function orp_deactivate() {
delete_transient( 'orp_notice_cache' );
}
register_deactivation_hook( __FILE__, 'orp_deactivate' );
Первый аргумент, __FILE__, указывает на главный файл плагина. Наш плагин пока ничего не кеширует, но callback деактивации демонстрирует общий принцип: удаляйте временные артефакты, не трогая сохранённую опцию. Префикс orp_ у каждого имени функции и опции предотвращает конфликты с ядром и другими плагинами.
Почему код моего плагина не выполняется?
Код в файле плагина не выполняется сам по себе. WordPress вызывает функцию только после того, как вы привяжете её к хуку, — до этого момента файл инертен. add_action() принимает имя хука и вызываемую функцию, а также необязательный приоритет со значением по умолчанию 10.
function orp_print_notice() {
$notice = get_option( 'orp_notice_text' );
if ( $notice ) {
echo '<p class="orp-notice">' . esc_html( $notice ) . '</p>';
}
}
add_action( 'wp_footer', 'orp_print_notice' );
Когда тема вызывает wp_footer, WordPress выполняет все привязанные к нему функции, включая нашу. Откройте любую страницу фронтенда — и уведомление появится перед закрывающим тегом body.
Добавляем фильтр: изменить значение и вернуть его
Действие (action) позволяет вашей функции выполниться в определённой точке жизненного цикла WordPress; фильтр передаёт вашей функции значение и ожидает получить обратно изменённое. Callback-функция фильтра обязана возвращать значение. Если она ничего не возвращает, PHP вернёт null, WordPress передаст этот null дальше, и всё, что вы фильтровали, исчезнет с сайта. Забудьте return в callback для body_class — и элемент body лишится классов; забудьте его в the_content — и все записи отрисуются пустыми.
function orp_body_class( $classes ) {
if ( get_option( 'orp_notice_text' ) ) {
$classes[] = 'orp-has-notice';
}
return $classes;
}
add_filter( 'body_class', 'orp_body_class' );
Эта функция получает массив классов body, добавляет один класс, когда уведомление задано, и возвращает массив, чтобы темы могли по-разному оформлять страницы, пока уведомление активно.
Как создать страницу настроек в WordPress?
Страница настроек — это три регистрации: страница меню на хуке admin_menu, а также настройка вместе с её секцией и полем на хуке admin_init, как описано в главе про Settings API. add_options_page() размещает страницу в разделе «Настройки»; register_setting() задаёт имя опции и, что критически важно, её sanitize_callback.
function orp_settings_menu() {
add_options_page( 'Site Notice', 'Site Notice', 'manage_options',
'orp-site-notice', 'orp_settings_page_html' );
}
add_action( 'admin_menu', 'orp_settings_menu' );
function orp_settings_init() {
register_setting( 'orp_settings', 'orp_notice_text', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
) );
add_settings_section( 'orp_main', 'Notice', '__return_false', 'orp-site-notice' );
add_settings_field( 'orp_notice_text', 'Notice text',
'orp_notice_field_html', 'orp-site-notice', 'orp_main' );
}
add_action( 'admin_init', 'orp_settings_init' );
function orp_notice_field_html() {
$value = get_option( 'orp_notice_text', '' );
echo '<input type="text" name="orp_notice_text" value="'
. esc_attr( $value ) . '" class="regular-text">';
}
function orp_settings_page_html() {
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields( 'orp_settings' );
do_settings_sections( 'orp-site-notice' );
submit_button( 'Save Notice' );
?>
</form>
</div>
<?php
}
Форма отправляется на options.php, и сохранение обрабатывает ядро. Обратите внимание, что register_setting() также принимает аргумент default; мы задаём значение по умолчанию в хуке активации, поэтому выберите один механизм и придерживайтесь его.
Как безопасно обработать отправленное значение?
Проверка прав (capability) отвечает на вопрос, разрешено ли этому пользователю сохранять настройки; nonce отвечает на вопрос, действительно ли пользователь намеревался отправить именно эту форму. Странице настроек, остающейся на клиентском сайте, нужны оба механизма, и nonce ни при каких условиях не заменяет проверку прав.
В нашем случае Settings API берёт большую часть работы на себя: settings_fields() выводит nonce, а ядро проверяет его при возврате формы, и ядро же блокирует сохранение, если у текущего пользователя нет права manage_options — той самой capability, которую wp-admin/options.php по умолчанию применяет к страницам настроек. Если же вы когда-нибудь напишете собственный обработчик формы, выводите nonce через wp_nonce_field() и проверяйте его через check_admin_referer(). Добавьте явную проверку в callback страницы, по образцу примера из официального справочника:
function orp_settings_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// ... form as above ...
}
Проверяйте право (capability), а не название роли вроде administrator. Оставшиеся два уровня уже реализованы: санитизация на входе в базу данных (sanitize_text_field в качестве sanitize_callback) и экранирование на выходе (esc_attr() в поле, esc_html() в подвале). Эти операции не взаимозаменяемы, и пропуск любой из них — прямой путь к тому, чтобы сохранённая опция превратилась в вектор инъекции.
Активируем и проверяем работу
Активируйте Site Notice на экране «Плагины», откройте фронтенд и проверьте наличие уведомления в подвале. Измените текст в разделе «Настройки → Site Notice» и перезагрузите страницу. Если ничего не происходит, пройдитесь по списку по порядку:
- Плагин действительно активирован, а не просто присутствует в папке?
- Каждая строка, переданная в
add_action()илиadd_filter(), точно совпадает с именем определённой функции? - Нет ли пробелов или вывода перед
<?php? В таком случае WordPress при активации сообщает об «unexpected output». - Включите
WP_DEBUGвwp-config.phpвместе сWP_DEBUG_LOG, записывающим ошибки вwp-content/debug.log, и прочитайте реальную ошибку PHP вместо того, чтобы гадать.
Теперь у вас есть плагин со значением опции по умолчанию, действием, фильтром, который возвращает своё значение, и страницей настроек, сохранение на которой защищено nonce, проверкой прав и санитайзером. Это тот базовый шаблон, который стоит воспроизводить для каждого сниппета, всё ещё живущего в functions.php: перенесите его, добавьте префикс, привяжите к хуку и сохраните связку «санитизация на входе — экранирование на выходе».
Часто задаваемые вопросы
В чём разница между wp_verify_nonce и check_admin_referer?
check_admin_referer() проверяет и nonce, и реферер для форм и URL на административных экранах, а при неудачной проверке прерывает запрос с кодом 403. wp_verify_nonce() проверяет только nonce и возвращает результат, который вы обрабатываете сами, что подходит для Ajax-обработчиков и других нестандартных сценариев. Ни одна из этих функций не заменяет проверку прав: nonce подтверждает, что пользователь намеревался выполнить действие, а current_user_can() подтверждает, что ему это разрешено.
Что происходит с сохранёнными опциями плагина при его деактивации?
Ничего: при деактивации опции остаются в базе данных, поэтому настройки сохранятся, когда пользователь снова включит плагин. Callback деактивации должен очищать только недолговечные данные, такие как transients или закешированные файлы. Окончательная очистка относится к процедуре удаления, реализуемой либо через register_uninstall_hook(), либо через файл uninstall.php в папке плагина. Если вы используете uninstall.php, он обязан проверить, определена ли константа WP_UNINSTALL_PLUGIN, прежде чем что-либо удалять.
Может ли плагин WordPress состоять более чем из одного PHP-файла?
Да, но комментарий-заголовок плагина должен содержаться только в главном файле. WordPress находит плагины, считывая PHP-файлы в каталоге плагинов в поисках этого заголовка, и каждый файл с заголовком отображается как отдельный плагин. Подключайте дополнительные файлы из главного файла через require_once, а вызовы вроде register_activation_hook() оставляйте привязанными к главному файлу плагина, поскольку их первый параметр должен ссылаться именно на него.
Где задавать значение опции по умолчанию: в register_setting или в хуке активации?
Используйте один механизм, а не оба. register_setting() принимает аргумент default, который возвращается, если значения нет в базе данных, тогда как add_option() в хуке активации один раз записывает реальную строку в таблицу при активации. Подход с активацией гарантирует, что значение существует при каждом запросе, включая запросы фронтенда; значение по умолчанию из register_setting() применяется только там, где выполнился код регистрации. Сочетание обоих подходов создаёт два источника истины для одной и той же опции.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k