Свой плагин для WordPress: рабочие приёмы из практики
Код в functions.php пропадает вместе с темой, а плагин без дисциплины конфликтует с соседями. Разбираем три способа организовать код для WordPress 6.x и приёмы, которые отсекают типичные ошибки.

Код в functions.php темы живёт до первой смены оформления: сайт переехал на новую тему, шорткоды вывелись текстом в квадратных скобках, а форма заявок перестала отправляться. Вынос кода в плагин это лечит, но приносит свои проблемы: функции с одинаковыми именами в двух плагинах, скрипты на каждой странице, настройки без проверки прав. Ниже три способа организовать код плагина для WordPress 6.x и приёмы, которые отсекают эти ошибки.
Основа: заголовок и защита входа
Все примеры собраны вокруг одной задачи: добавить в конец записи строку «Обновлено: дата». Папка плагина: wp-content/plugins/acme-updated-note/, главный файл: acme-updated-note.php. Код рассчитан на PHP 8.0 и выше.
<?php
/**
* Plugin Name: Acme Updated Note
* Description: Добавляет в конец записи строку с датой обновления.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
* Text Domain: acme-updated-note
*/
defined( 'ABSPATH' ) || exit;
WordPress находит плагин по комментарию с полем Plugin Name. Остальные поля необязательны, но Requires PHP и Requires at least избавят от белого экрана на старом хостинге: плагин просто не активируется. Строка с ABSPATH закрывает прямой вызов файла по URL.
Три способа организовать код
Функции с префиксом
function acme_un_append_note( $content ) {
// Только основной контент одиночной записи
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
$date = get_the_modified_date( 'j F Y' );
return $content . '<p class="acme-un">Обновлено: ' . esc_html( $date ) . '</p>';
}
add_filter( 'the_content', 'acme_un_append_note' );
Выигрыш: никакой церемонии, читается за минуту. Цена: защита от конфликтов держится на вашей дисциплине. Функция send_mail() без префикса в двух плагинах даёт Fatal error: Cannot redeclare send_mail(), и сайт падает на активации второго.
Класс
Схематично: хуки собраны в одном месте, общее состояние лежит в свойствах, а не в глобальных переменных.
final class Acme_UN_Plugin {
public static function init(): void {
add_filter( 'the_content', array( self::class, 'append_note' ) );
}
public static function append_note( string $content ): string {
// тело то же, что в acme_un_append_note() выше
return $content;
}
}
add_action( 'plugins_loaded', array( 'Acme_UN_Plugin', 'init' ) );
Имя класса тоже нужно префиксовать, поэтому конфликт имён уходит не полностью. Зато видна вся точка входа, а регистрация на plugins_loaded позволяет проверить наличие нужных плагинов до запуска.
Пространство имён и Composer
В composer.json описывается автозагрузка PSR-4, классы лежат в src/ и объявлены с namespace Acme\UpdatedNote;.
{
"autoload": {
"psr-4": { "Acme\\UpdatedNote\\": "src/" }
}
}
После composer install в главном файле остаются две строки:
require_once __DIR__ . '/vendor/autoload.php';
add_action( 'plugins_loaded', array( \Acme\UpdatedNote\Plugin::class, 'init' ) );
Цена: папку vendor/ нужно упаковывать в релиз. Если два плагина тянут разные версии одной библиотеки, загрузится та, что подключена первой, и второй плагин получит чужой код. Для таких случаев есть Strauss и PHP-Scoper: они переименовывают пространства имён зависимостей.
| Подход | Выигрыш | Цена |
|---|---|---|
| Функции с префиксом | Минимум кода, нет сборки | Конфликты имён на совести автора |
| Класс | Одна точка входа, общее состояние | Файл разрастается без разбиения |
| Namespace + Composer | Автозагрузка, тесты, сторонние пакеты | Сборка релиза, риск конфликта версий библиотек |
Приёмы, которые экономят часы
Ограничивайте контекст фильтра
Фильтр the_content вызывается не только на странице записи: тема может прогнать через него текст в архиве или виджете, а REST API использует его для поля content.rendered. Проверки is_singular(), in_the_loop() и is_main_query() в первом примере нужны именно поэтому.
Скрипты только там, где они нужны
add_action( 'wp_enqueue_scripts', function () {
if ( ! is_singular( 'post' ) ) {
return;
}
wp_enqueue_script(
'acme-un',
plugin_dir_url( __FILE__ ) . 'assets/note.js',
array(),
'1.0.0',
array( 'strategy' => 'defer', 'in_footer' => true )
);
} );
Ключ strategy в пятом аргументе работает с WordPress 6.3. В более ранних версиях там булево значение in_footer.
Форма в админке: права, nonce, очистка
Форма отправляется на admin-post.phpwp_nonce_field( 'acme_un_save' ) и скрытое поле action со значением acme_un_save. Обработчик проверяет права до nonce и чистит ввод:
add_action( 'admin_post_acme_un_save', function () {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Недостаточно прав.', 'acme-updated-note' ), '', array( 'response' => 403 ) );
}
check_admin_referer( 'acme_un_save' );
$label = isset( $_POST['label'] )
? sanitize_text_field( wp_unslash( $_POST['label'] ) )
: '';
update_option( 'acme_un_label', $label );
wp_safe_redirect( admin_url( 'options-general.php?page=acme-un' ) );
exit;
} );
Для простых настроек Settings API (register_setting() с sanitize_callback) даёт то же с меньшим количеством кода. Обработчик через admin_post_* нужен для нестандартных форм. Правило одно: чистим на входе, экранируем на выходе (esc_html, esc_attr, esc_url).
Уборка за собой
Деактивация не должна стирать данные: плагин могут выключить на час ради проверки. Удаление данных живёт в отдельном файле uninstall.php рядом с главным:
<?php
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
delete_option( 'acme_un_label' );
Плагин должен вести себя на чужом сайте как гость: не занимать общие имена, не работать там, где его не звали, и убирать за собой.
Когда что выбирать
- Функции с префиксом: один файл, один автор, до пары сотен строк. Это мой ориентир, в документации WordPress такого порога нет.
- Класс: несколько хуков делят настройки или состояние, код разросся на несколько файлов.
- Namespace и Composer: над плагином работает больше одного человека, нужны юнит-тесты или сторонние библиотеки.
Что ломается
- Белый экран после активации. Включите в
wp-config.phpконстантыWP_DEBUGиWP_DEBUG_LOG, ошибка появится вwp-content/debug.log. На проде после проверки отладку выключите. - 404 на записях нового типа. После регистрации типа записи обновите правила ссылок: «Настройки → Постоянные ссылки → Сохранить» или один вызов
flush_rewrite_rules()в хуке активации. На каждый запрос её ставить нельзя: она дорогая. - Браузер отдаёт старый скрипт. Передавайте в
wp_enqueue_script()версию плагина: приfalseWordPress подставит версию ядра, и после обновления плагина кеш не сбросится. - Уведомление о слишком раннем вызове перевода. В WordPress 6.7 и новее обращение к
__()до хукаinitсообщает об ошибке; не вызывайте переводы в теле файла. - Тормоза на каждой странице. Большой массив в опции с автозагрузкой читается при каждом запросе. Передайте третьим аргументом
update_option()значениеfalse.
Что делать дальше
Прогоните готовый плагин через три инструмента. Plugin Check от команды WordPress.org найдёт нарушения требований к плагинам. PHP_CodeSniffer с набором WordPress Coding Standards поймает неэкранированный вывод и пропущенные проверки nonce. Query Monitor покажет, сколько запросов к базе и хуков добавил ваш код: замерьте их число на типичной странице до и после включения плагина. Тесты хуков и создание таблиц через dbDelta() здесь не разбираем, это отдельные темы.
Хук (hook) в WordPress: точка в ядре, к которой плагин подключает свою функцию. Action выполняет действие, filter получает значение и возвращает изменённое. Nonce: токен с ограниченным временем жизни (по умолчанию сутки), который привязывает запрос к пользователю и защищает форму от подделки с чужого сайта.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Plugin Developer Handbook, WordPress.org, WordPress Code Reference
Частые вопросы
А если у меня WordPress старше 6.3 или 6.7?
Плагин не появился в списке или не активируется. Что проверить?
Что лучше: плагин, mu-plugin или functions.php?
Какие подводные камни ждут плагин в проде?
Нужно ли удалять данные плагина при его удалении?
Почему мой фильтр the_content срабатывает не там, где нужно?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.