Разработка кастомного плагина WordPress
Когда готовый плагин делает 90% нужного, а оставшиеся 10% не реализовать без взлома его кода — пора писать собственный. Кастомный плагин — это не обязательно мегабиблиотека: иногда это 200 строк кода, которые добавляют конкретную бизнес-логику, недоступную в маркетплейсе. Разработка плагина средней сложности занимает от 3 до 10 рабочих дней.
Структура плагина
WordPress не требует строгой структуры папок, но есть устоявшиеся конвенции:
wp-content/plugins/my-plugin/
├── my-plugin.php # Главный файл, точка входа
├── includes/
│ ├── class-my-plugin.php # Основной класс
│ ├── class-my-plugin-admin.php # Логика для /wp-admin
│ └── class-my-plugin-public.php # Логика для фронтенда
├── admin/
│ ├── css/admin.css
│ └── js/admin.js
├── public/
│ ├── css/public.css
│ └── js/public.js
└── languages/
└── my-plugin-ru_RU.po
Главный файл содержит заголовок и bootstrapping:
<?php
/**
* Plugin Name: My Custom Plugin
* Plugin URI: https://example.com/my-plugin
* Description: Описание функциональности плагина.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.1
* Author: Company Name
* Text Domain: my-plugin
*/
if (!defined('ABSPATH')) {
exit;
}
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
require_once MY_PLUGIN_DIR . 'includes/class-my-plugin.php';
function my_plugin_init(): void {
$plugin = new My_Plugin();
$plugin->run();
}
add_action('plugins_loaded', 'my_plugin_init');
Основной класс и реестр хуков
Паттерн «Loader» — регистрация всех хуков в одном месте вместо разброса add_action по файлам:
class My_Plugin {
private array $actions = [];
private array $filters = [];
public function __construct() {
$this->define_admin_hooks();
$this->define_public_hooks();
}
private function define_admin_hooks(): void {
$admin = new My_Plugin_Admin();
$this->add_action('admin_enqueue_scripts', $admin, 'enqueue_styles');
$this->add_action('admin_menu', $admin, 'add_plugin_admin_menu');
}
private function add_action(string $hook, object $component, string $callback, int $priority = 10): void {
$this->actions[] = compact('hook', 'component', 'callback', 'priority');
}
public function run(): void {
foreach ($this->actions as $hook) {
add_action($hook['hook'], [$hook['component'], $hook['callback']], $hook['priority']);
}
foreach ($this->filters as $hook) {
add_filter($hook['hook'], [$hook['component'], $hook['callback']], $hook['priority'], $hook['accepted_args'] ?? 1);
}
}
}
Работа с базой данных
Для кастомных таблиц — создание через dbDelta при активации плагина:
register_activation_hook(__FILE__, function () {
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_data';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
data_key varchar(255) NOT NULL,
data_value longtext,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY data_key (data_key)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
add_option('my_plugin_db_version', MY_PLUGIN_VERSION);
});
dbDelta умеет обновлять существующие таблицы при обновлении плагина — не удаляет данные, только добавляет недостающие столбцы и индексы.
Для запросов — только через $wpdb->prepare(), никаких конкатенаций строк с пользовательскими данными:
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_plugin_data WHERE user_id = %d AND data_key = %s",
get_current_user_id(),
$key
)
);
Страница настроек в админке
Settings API — стандартный способ добавить страницу настроек с нативным интерфейсом WordPress:
class My_Plugin_Admin {
public function add_plugin_admin_menu(): void {
add_options_page(
'Настройки My Plugin',
'My Plugin',
'manage_options',
'my-plugin',
[$this, 'display_plugin_setup_page']
);
}
public function __construct() {
add_action('admin_init', [$this, 'register_settings']);
}
public function register_settings(): void {
register_setting('my_plugin_options', 'my_plugin_api_key', [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
]);
add_settings_section('my_plugin_main', 'Основные настройки', null, 'my-plugin');
add_settings_field('api_key', 'API ключ', function () {
$value = get_option('my_plugin_api_key', '');
printf('<input type="text" name="my_plugin_api_key" value="%s" class="regular-text">', esc_attr($value));
}, 'my-plugin', 'my_plugin_main');
}
public function display_plugin_setup_page(): void {
if (!current_user_can('manage_options')) {
return;
}
echo '<div class="wrap"><h1>' . esc_html(get_admin_page_title()) . '</h1>';
echo '<form method="post" action="options.php">';
settings_fields('my_plugin_options');
do_settings_sections('my-plugin');
submit_button();
echo '</form></div>';
}
}
AJAX-обработчики
// Регистрация обработчика
add_action('wp_ajax_my_plugin_action', [$this, 'handle_ajax']);
add_action('wp_ajax_nopriv_my_plugin_action', [$this, 'handle_ajax']); // для незалогиненных
public function handle_ajax(): void {
check_ajax_referer('my_plugin_nonce', 'nonce');
$input = sanitize_text_field($_POST['data'] ?? '');
// бизнес-логика
$result = $this->process($input);
wp_send_json_success(['result' => $result]);
// или wp_send_json_error(['message' => 'Ошибка'], 400);
}
На клиенте:
fetch(childTheme.ajaxUrl, {
method: 'POST',
body: new URLSearchParams({
action: 'my_plugin_action',
nonce: childTheme.nonce,
data: inputValue,
}),
})
.then(r => r.json())
.then(r => { if (r.success) console.log(r.data.result) });
REST API вместо AJAX
Для современных интерфейсов на React/Vue предпочтительнее REST API. Подробнее — в отдельной услуге «Разработка кастомных REST API эндпоинтов WordPress».
Интернационализация
Все строки в плагине должны быть обёрнуты в функции перевода с указанием text domain:
__('Строка для перевода', 'my-plugin')
_e('Вывести строку', 'my-plugin')
esc_html__('Безопасный вывод', 'my-plugin')
sprintf(__('Привет, %s!', 'my-plugin'), $username)
.pot-файл генерируется через WP-CLI: wp i18n make-pot . languages/my-plugin.pot.
Деактивация и удаление
register_deactivation_hook(__FILE__, function () {
// Убираем cron-задания, временные данные
wp_clear_scheduled_hook('my_plugin_cron');
});
register_uninstall_hook(__FILE__, 'my_plugin_uninstall');
function my_plugin_uninstall(): void {
// Удаляем только если пользователь согласился
if (get_option('my_plugin_delete_data_on_uninstall')) {
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}my_plugin_data");
delete_option('my_plugin_api_key');
}
}
Типовые сроки по сложности
Простой плагин (шорткод + страница настроек) — 2–3 дня. Плагин со своими таблицами, AJAX-интерфейсом и интеграцией со сторонним API — 5–8 дней. Плагин с метабоксами, кастомными таблицами в списке записей, сложной логикой ролей и правил — от 10 дней.







