В разработке на WordPress начинающие специалисты часто смешивают уровень представления и бизнес-логику, помещая регистрацию типов записей (Custom Post Types), создание кастомных таблиц MySQL и интеграцию со сторонними API в файл functions.php темы.
Последствия нарушения архитектурных границ:
function init(), function save_data(), неизбежно конфликтуют с другими плагинами, приводя к фатальным сбоям Fatal error: Cannot redeclare function.wp_options сотнями устаревших записей, которые остаются в базе навсегда даже после удаления плагина через админку. Либо, наоборот, стирают пользовательские данные при банальном обновлении плагина через деактивацию.Профессиональная разработка плагинов строится на принципах объектно-ориентированного программирования (ООП), изоляции в пространствах имен (Namespaces), следовании стандарту автозагрузки PSR-4 и четком контроле жизненного цикла (Plugin Lifecycle).
Плагин — это независимый программный модуль бизнес-логики (Business Logic Layer).
Он обязан функционировать независимо от того, какая тема сейчас активна на сайте. Плагин регистрирует структуры данных (CPT, таксономии, поля, таблицы), настраивает эндпоинты REST API, обрабатывает вебхуки и предоставляет точки расширения (Action и Filter Hooks) для других разработчиков.
Каждый плагин WordPress управляется ядром через стандартизированный протокол жизненного цикла, состоящий из пяти изолированных фаз.
┌────────────────────────────────────────────────────────────────────────┐
│ ЖИЗНЕННЫЙ ЦИКЛ ПЛАГИНА │
└────────────────────────────────────────────────────────────────────────┘
│
▼
1. УСТАНОВКА (Скачивание и распаковка в wp-content/plugins/)
│
▼
2. АКТИВАЦИЯ (register_activation_hook)
├── Проверка версии PHP (>= 8.2) и ядра WordPress (>= 6.4)
├── Создание/миграция кастомных таблиц через dbDelta()
├── Запись дефолтных настроек в wp_options
└── Однократный сброс постоянных ссылок: flush_rewrite_rules(false)
│
▼
3. ВЫПОЛНЕНИЕ / RUNTIME (Каждый HTTP / CLI / REST запрос)
├── Инициализация автозагрузчика PSR-4
├── plugins_loaded: создание инстанса ядра плагина
├── init: регистрация CPT, таксономий, ролей, шорткодов
├── rest_api_init: регистрация контроллеров эндпоинтов
└── admin_menu / wp_enqueue_scripts: интерфейс и ассеты
│
▼
4. ДЕАКТИВАЦИЯ (register_deactivation_hook)
├── Очистка запланированных задач cron: wp_clear_scheduled_hook()
├── Очистка транзиентного кэша: delete_transient()
├── Однократный сброс постоянных ссылок: flush_rewrite_rules(false)
└── ⛔ ЗАПРЕЩЕНО: УДАЛЯТЬ ТАБЛИЦЫ БД ИЛИ НАСТРОЙКИ КЛИЕНТА!
│
▼
5. УДАЛЕНИЕ / ДЕИНСТАЛЛЯЦИЯ (uninstall.php)
├── Защитная проверка константы WP_UNINSTALL_PLUGIN
├── Полная очистка таблиц: DROP TABLE wp_agenticlab_data
├── Удаление метаданных постов и пользователей
└── Удаление всех опций плагина из таблицы wp_optionsГлавный файл плагина размещается в общедоступной веб-директории сервера. Если злоумышленник обратится к нему напрямую по адресу https://site.com/wp-content/plugins/my-plugin/my-plugin.php, файл исполнится вне контекста ядра WordPress, что может привести к утечке путей сервера (Full Path Disclosure) или выполнению уязвимого кода.
Поэтому первой строкой каждого исполняемого PHP-файла плагина обязана быть проверка:
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit; // Немедленное завершение, если ядро WordPress не инициализировано
}Создание директории плагина и главного файла с заголовком: Plugin Name, Plugin URI, Version, Requires PHP, License и Text Domain для интернационализации.
Имя директории и главного файла должны быть идентичными в нижнем регистре (например, agenticlab-core/agenticlab-core.php).
Организация классов в каталоге src/ с корневым пространством имен (например, AgenticLab\Core\). Подключение автозагрузчика исключает спагетти из десятков require_once.
Автозагрузка загружает классы только тогда, когда к ним происходит реальное обращение в памяти.
Создание класса-активатора: валидация совместимости окружения (PHP 8.2+), развертывание схемы таблиц MySQL через ядровый dbDelta() и безопасный сброс rewrite rules.
Вызов flush_rewrite_rules допустим исключительно в хуке активации!
Разделение логики на изолированные сервисы (AdminMenu, RestApiController, PostTypeRegistrar, AssetsManager) с подпиской на хуки WordPress через типизированные методы.
Каждый сервис решает строго одну задачу (Single Responsibility Principle).
Все операции записи и мутации данных защищаются проверкой криптографических одноразовых токенов (Nonce) и верификацией прав текущего пользователя.
Никогда не доверяйте входящим массивам $_POST и $_GET без валидации и санитизации.
Создание изолированного скрипта в корне плагина. Скрипт вызывается ядром только при физическом нажатии кнопки 'Удалить' и зачищает таблицы БД и опции.
Использование хука деактивации для очистки базы данных считается грубейшей ошибкой.
Почему категорически запрещено удалять таблицы базы данных и настройки плагина внутри хука register_deactivation_hook()?
ООП-архитектура плагинов используется во всех enterprise-решениях экосистемы WordPress:
subscriber_pro, manager), эндпоинтов REST API для мобильных приложений и закрытых страниц доступа с проверкой прав current_user_can().Эталонная файловая структура современного ООП-плагина со стандартом PSR-4:
wp-content/plugins/agenticlab-core/
├── assets/ # Статические ассеты плагина
│ ├── css/admin.css
│ └── js/admin.js
├── src/ # Исходный код на PHP 8.2+ (PSR-4: AgenticLab\Core\)
│ ├── Admin/
│ │ └── SettingsPage.php # Настройки в консоли (Settings API)
│ ├── Api/
│ │ └── RestController.php # Кастомные эндпоинты WP REST API
│ ├── Database/
│ │ └── Migrator.php # Создание таблиц MySQL через dbDelta()
│ ├── PostTypes/
│ │ └── PortfolioPostType.php # Регистрация CPT и таксономий
│ └── Plugin.php # Главный оркестратор сервисов
├── composer.json # Манифест зависимостей и маппинг PSR-4
├── uninstall.php # Изолированный деинсталлятор
└── agenticlab-core.php # Главная точка входа (Bootstrap)Ядро WordPress считывает заголовки agenticlab-core.php, проверяет совместимость версий PHP и WP, регистрирует хуки активации и деактивации.
Регистрируется автозагрузчик пространств имен AgenticLab\Core\. Классы подключаются по требованию из каталога src/ без единого лишнего require_once.
На системном хуке plugins_loaded вызывается метод AgenticLab\Core\Plugin::instance()->boot(), который инстанциирует зарегистрированные сервисы плагина.
Сервисы подписываются на хуки ядра: PortfolioPostType регистрирует CPT на init, SettingsPage добавляет меню на admin_menu, RestController подключается на rest_api_init.
При удалении плагина администратором ядро вызывает uninstall.php. Проверяется константа безопасности, удаляются таблицы MySQL и ключи опций.
Функция ядра dbDelta() крайне чувствительна к синтаксису SQL-запроса CREATE TABLE. Какое из следующих правил является обязательным для корректной работы dbDelta()?
Ниже представлены ключевые файлы эталонного ООП-плагина на PHP 8.2+, готового к развертыванию в Production.
agenticlab-core.php#<?php
/**
* Plugin Name: Fullstack Agentic Lab Core Engine
* Plugin URI: https://agenticlab.dev/workflows/wordpress/wf-plugin-development
* Description: Высокопроизводительный ООП-плагин для платформы Fullstack Agentic Lab (PHP 8.2+, PSR-4, Custom DB Tables, REST API).
* Version: 1.0.0
* Requires at least: 6.4
* Requires PHP: 8.2
* Author: Artur Yusupov (Fullstack Agentic Lab)
* Author URI: https://agenticlab.dev
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: agenticlab-core
* Domain Path: /languages
*
* @package AgenticLab\Core
*/
declare(strict_types=1);
namespace AgenticLab\Core;
if (!defined('ABSPATH')) {
exit;
}
// Константы плагина
define('AGENTIC_CORE_VERSION', '1.0.0');
define('AGENTIC_CORE_FILE', __FILE__);
define('AGENTIC_CORE_DIR', plugin_dir_path(__FILE__));
define('AGENTIC_CORE_URI', plugin_dir_url(__FILE__));
// Проверка версии PHP перед инициализацией
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
add_action('admin_notices', function (): void {
echo '<div class="error"><p>' . esc_html__('Плагин Fullstack Agentic Lab Core требует PHP версии 8.2 или выше.', 'agenticlab-core') . '</p></div>';
});
return;
}
// 1. Автозагрузчик классов по стандарту PSR-4
spl_autoload_register(function (string $class): void {
$prefix = 'AgenticLab\\Core\\';
$base_dir = AGENTIC_CORE_DIR . 'src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// 2. Регистрация хука активации
register_activation_hook(__FILE__, function (): void {
Database\Migrator::migrate();
// Безопасный однократный сброс rewrite rules
if (!get_option('agenticlab_core_flush_rewrite')) {
update_option('agenticlab_core_flush_rewrite', true);
}
});
// 3. Регистрация хука деактивации (только сброс временных данных!)
register_deactivation_hook(__FILE__, function (): void {
wp_clear_scheduled_hook('agenticlab_core_daily_cron');
flush_rewrite_rules(false);
});
// 4. Запуск ядра плагина на хуке plugins_loaded
add_action('plugins_loaded', function (): void {
Plugin::instance()->boot();
});src/Database/Migrator.php#<?php
/**
* Database Migration Handler via dbDelta.
*
* @package AgenticLab\Core\Database
*/
declare(strict_types=1);
namespace AgenticLab\Core\Database;
if (!defined('ABSPATH')) {
exit;
}
final class Migrator {
public static function get_table_name(): string {
global $wpdb;
return $wpdb->prefix . 'agenticlab_logs';
}
/**
* Создает или обновляет схему таблицы без потери существующих данных.
*/
public static function migrate(): void {
global $wpdb;
$table_name = self::get_table_name();
$charset_collate = $wpdb->get_charset_collate();
// ВАЖНО: Соблюдение строгих правил dbDelta:
// 1. Каждое поле на отдельной строке.
// 2. PRIMARY KEY с ДВУМЯ пробелами перед скобкой!
// 3. Ключевые слова типов полей в ВЕРХНЕМ регистре.
$sql = "CREATE TABLE {$table_name} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
event_type VARCHAR(64) NOT NULL DEFAULT '',
payload LONGTEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY event_type (event_type)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option('agenticlab_db_version', AGENTIC_CORE_VERSION);
}
}src/Plugin.php#<?php
/**
* Main Plugin Orchestrator.
*
* @package AgenticLab\Core
*/
declare(strict_types=1);
namespace AgenticLab\Core;
if (!defined('ABSPATH')) {
exit;
}
final class Plugin {
private static ?self $instance = null;
private bool $booted = false;
public static function instance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
/**
* Инициализация компонентов и подписка на события ядра.
*/
public function boot(): void {
if ($this->booted) {
return;
}
$this->booted = true;
add_action('init', [$this, 'on_init']);
add_action('rest_api_init', [$this, 'on_rest_api_init']);
add_action('admin_enqueue_scripts', [$this, 'on_admin_assets']);
}
public function on_init(): void {
// Регистрация кастомного типа записи Portfolio
register_post_type('agentic_portfolio', [
'labels' => [
'name' => esc_html__('Портфолио', 'agenticlab-core'),
'singular_name' => esc_html__('Проект', 'agenticlab-core'),
],
'public' => true,
'has_archive' => true,
'show_in_rest' => true, // Включение редактора Gutenberg и REST API
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
'menu_icon' => 'dashicons-portfolio',
]);
// Отложенный однократный сброс rewrite rules после активации
if (get_option('agenticlab_core_flush_rewrite')) {
delete_option('agenticlab_core_flush_rewrite');
flush_rewrite_rules(false);
}
}
public function on_rest_api_init(): void {
register_rest_route('agenticlab/v1', '/health', [
'methods' => 'GET',
'callback' => function (): array {
return [
'status' => 'ok',
'version' => AGENTIC_CORE_VERSION,
'timestamp' => time(),
];
},
'permission_callback' => '__return_true',
]);
}
public function on_admin_assets(): void {
wp_enqueue_style(
'agenticlab-admin-css',
AGENTIC_CORE_URI . 'assets/css/admin.css',
[],
AGENTIC_CORE_VERSION
);
}
}uninstall.php#<?php
/**
* Plugin Uninstall Handler.
* Triggered ONLY when the user clicks 'Delete' in the WordPress admin panel.
*
* @package AgenticLab\Core
*/
declare(strict_types=1);
// Защита: прерываем выполнение, если вызов сделан не ядром при деинсталляции
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
global $wpdb;
// 1. Безвозвратное удаление кастомной таблицы плагина
$table_name = $wpdb->prefix . 'agenticlab_logs';
$wpdb->query("DROP TABLE IF EXISTS {$table_name}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// 2. Удаление сохраненных опций из таблицы wp_options
delete_option('agenticlab_db_version');
delete_option('agenticlab_core_flush_rewrite');
// 3. Очистка транзиентов плагина
delete_transient('agenticlab_core_cache');
// 4. Опционально: удаление всех постов типа agentic_portfolio
$posts = get_posts([
'post_type' => 'agentic_portfolio',
'numberposts' => -1,
'post_status' => 'any',
'fields' => 'ids',
]);
foreach ($posts as $post_id) {
wp_delete_post($post_id, true); // true = принудительное удаление мимо корзины
}Почему разработчики так делают: Разработчик считает, что хук деактивации — это аналог «выключения», при котором за собой нужно прибрать все следы в базе данных.
Что происходит в продакшене: Когда администратор нажимает кнопку «Обновить плагин» в консоли WordPress, ядро в автоматическом режиме:
Если вы удалили таблицы в деактивации, обновление плагина мгновенно уничтожит все коммерческие данные пользователей (заказы, логи, настройки).
Как правильно: В хуке деактивации только сбрасывать фоновые задачи (wp_clear_scheduled_hook) и временный кэш. Полное удаление таблиц и опций производить строго в файле uninstall.php.
Почему разработчики так делают: После регистрации CPT одиночные страницы выдают 404. Разработчик находит совет на форуме и вставляет flush_rewrite_rules() прямо в метод регистрации CPT на хуке init.
Что происходит в продакшене:
Функция flush_rewrite_rules() разбирает сотни регулярных выражений маршрутизации и производит тяжелую дисковую запись в базу данных (в поле rewrite_rules таблицы wp_options). Вызов этой функции на каждом хите увеличивает время ответа сервера (TTFB) на 200–500 мс и убивает производительность при первом же наплыве трафика.
Как правильно: Вызывать сброс правил строго однократно — при активации плагина или через флаг отложенного сброса в опциях:
// ПРАВИЛЬНО: Однократный сброс через опцию-флаг
if (get_option('my_plugin_flush_needed')) {
delete_option('my_plugin_flush_needed');
flush_rewrite_rules(false); // false = мягкий сброс без перезаписи .htaccess
}Почему разработчики так делают: Разработчик пишет стандартный SQL-запрос CREATE TABLE так, как привык в MySQL CLI или phpMyAdmin.
Что происходит в продакшене:
Функция dbDelta() не является полноценным SQL-компилятором — это парсер на регулярных выражениях. Если не соблюсти ее специфические правила, функция не выдаст ошибку, но таблица просто не создастся в базе данных, что приведет к скрытым багам в рантайме.
Как правильно:
BIGINT, VARCHAR, NOT NULL) заглавными буквами.PRIMARY KEY должно быть ровно два пробела перед скобкой: PRIMARY KEY (id).KEY, а не INDEX: KEY user_id (user_id).Почему разработчики так делают: Разработчик принимает POST-запрос напрямую из $_POST['action'] и сразу меняет статус заказа или сохраняет настройки.
Что происходит в продакшене:
Сайт становится уязвимым для CSRF (Cross-Site Request Forgery) и эскалации привилегий. Злоумышленник может заманить авторизованного администратора на сторонний сайт, скрытый скрипт с которого отправит фоновый запрос на site.com/wp-admin/admin-post.php и изменит критические настройки магазина.
Как правильно: Всегда проверять криптографический токен (Nonce) и права пользователя:
// 1. Проверка прав пользователя
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Недостаточно прав.', 'agenticlab-core'));
}
// 2. Проверка CSRF Nonce токена
check_admin_referer('agenticlab_save_settings_action', 'agenticlab_nonce_field');uninstall.php — за окончательное удаление данных.dbDelta() с соблюдением ее строгих синтаксических правил.flush_rewrite_rules() на хуке init является грубейшим антипаттерном производительности и допустим только однократно при активации.check_admin_referer() (CSRF-защита) и current_user_can() (проверка прав доступа).