Архитектура современного корпоративного плагина в WordPress 6.7–7.0+ («Armstrong») ушла далеко от монолитных процедурных файлов эпохи PHP 5/7. Промышленный стандарт построения плагинов (Enterprise Plugin Boilerplate) опирается на 5 архитектурных инвариантов:
Requires Plugins: Header): Нативный механизм WordPress (введен в WP 6.5 и стандартизирован в WP 7.0), позволяющий указать в заголовке главного файла плагина список обязательных плагинов-зависимостей (Requires Plugins: woocommerce, advanced-custom-fields). Ядро автоматически блокирует активацию дочернего плагина и отключение родительского без написания кастомных проверок is_plugin_active().PHP-Scoper / Strauss): Кодовая база структурируется в директории src/ по стандарту Composer PSR-4, а все сторонние библиотеки из vendor/ компилируются с уникальным префиксом пространства имен (AgenticLab\Vendor\...), полностью исключая фатальные конфликты версий между разными плагинами.dbDelta(): Создание и обновление кастомных таблиц выполняется строго при активации или обновлении версии плагина (SCHEMA_VERSION в wp_options) с соблюдением жестких правил форматирования SQL-запросов парсера dbDelta.Activation ≠ Deactivation ≠ Uninstall):
register_activation_hook: проверка окружения, миграция таблиц dbDelta, назначение прав ролей, мягкий сброс ЧПУ flush_rewrite_rules(false).register_deactivation_hook: остановка задач Action Scheduler / WP-Cron и сброс временных транзиентов (без удаления пользовательских данных!).uninstall.php: изолированный деструктор, запускаемый в чистом окружении при полном удалении плагина (defined('WP_UNINSTALL_PLUGIN')) для очистки кастомных таблиц, опций и метаданных.Жизненный цикл плагина следует воспринимать как детерминированный конечный автомат переходов между состояниями установки, работы и деинсталляции, где каждая фаза имеет строгие границы допустимых побочных эффектов.
Requires Plugins Header PHP-Scoper / Strauss dbDelta Idempotent Activation Isolated uninstall.php[1. Установка ZIP / Git Deploy]
│
▼
[2. Проверка заголовков ядром WP 7.0+]
├── Requires PHP: 8.4
├── Requires at least: 6.7
└── Requires Plugins: woocommerce ──► Если нет: Блокировка активации в UI!
│
▼
[3. Фаза Активации: register_activation_hook]
├── Запуск миграций таблиц через dbDelta()
├── Сохранение версии схемы: update_option('agentic_db_version', '2.1.0')
└── flush_rewrite_rules(false)
│
▼
[4. Штатная работа: хук plugins_loaded]
├── Подключение vendor/scoper-autoload.php (PSR-4)
└── Проверка версии БД (Auto-Upgrade при обновлении плагина по Git/FTP)
│
▼
[5. Деактивация: register_deactivation_hook]
├── wp_clear_scheduled_hook('agentic_cron_sync')
└── Данные и таблицы пользователя СОХРАНЯЮТСЯ!
│
▼
[6. Полное удаление: uninstall.php]
├── Проверка if (!defined('WP_UNINSTALL_PLUGIN')) exit;
└── DROP TABLE + delete_option + очистка кэшаВалидация заголовков Requires PHP, Requires at least и Requires Plugins ядром до выполнения кода плагина.
Загрузка классов из src/ и изолированных зависимостей из build/vendor/ без загрязнения глобального скоупа.
Анализ текущей схемы таблиц MySQL, создание или добавление колонок/индексов через dbDelta и фиксация версии.
Снятие фоновых воркеров WP-Cron и очистка транзиентного кэша с полным сохранением данных клиента.
Изолированный запуск скрипта очистки при удалении плагина: удаление кастомных таблиц, опций и мета-ключей.
Ядро WordPress 7.0+ анализирует заголовки Requires PHP: 8.4 и Requires Plugins перед разрешением активации.
Исключает фатальные ошибки несовместимости окружения еще до запуска кода.
Создание таблиц через dbDelta(), регистрация задач WP-Cron и мягкий сброс правил перезаписи ЧПУ.
При сетевой активации в Multisite цикл проходит по всем сайтам через switch_to_blog().
Сверка сохраненной версии БД в wp_options с константой VERSION в коде. При обновлении запускается dbDelta().
Гарантирует актуальность таблиц при деплое через Git и CI/CD без ручной реативации.
Остановка периодических событий wp_clear_scheduled_hook() и сброс кэша маршрутов.
Все таблицы базы данных и настройки клиента остаются неприкосновенными.
Проверка константы WP_UNINSTALL_PLUGIN и полная очистка таблиц, опций и мета-полей плагина.
Выполняется только тогда, когда администратор явно удаляет неактивный плагин.
Как механизм Requires Plugins в WordPress 6.5–7.0+ изменяет управление зависимостями между плагинами по сравнению с проверками через is_plugin_active()?
PHP-Scoper): Изоляция библиотек (Guzzle, Symfony, Monolog, Carbon) в собственное подпространство имен позволяет вашему плагину стабильно работать на клиентских сайтах рядом с сотней чужих плагинов любых версий.register_activation_hook не срабатывает, проверка константы SCHEMA_VERSION на plugins_loaded автоматически запускает dbDelta() при обнаружении новой версии кода.{$wpdb->prefix}agentic_telemetry с первичным ключом и B-Tree индексами.is_multisite()): Корректная инициализация и очистка таблиц для каждого подсайта сети WordPress Multisite через get_sites(['fields' => 'ids']) и switch_to_blog().Сравним устаревшую организацию плагина с современной модульной архитектурой WordPress 7.0+:
dbDelta()#Функция dbDelta() в wp-admin/includes/upgrade.php использует регулярные выражения для разбора SQL-строки и сравнения ее с выводом DESCRIBE table_name. Нарушение любого из следующих правил приведет к тому, что миграция тихо проигнорируется:
PRIMARY KEY и открывающей скобкой ( ОБЯЗАНО стоять ровно два пробела: PRIMARY KEY (id).KEY вместо синонима INDEX, обязательно указывая имя индекса: KEY status_created_idx (status, created_at).IF NOT EXISTS внутри CREATE TABLE и нельзя ставить обратные кавычки (backticks `) вокруг имен полей.Ниже представлен эталонный каркас главного файла плагина и деинсталлятора на PHP 8.4+ со встроенным PSR-4 загрузчиком, версионируемым мигратором dbDelta и поддержкой WordPress Multisite.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Core Infrastructure Plugin
* Plugin URI: https://agenticlab.dev/lab
* Description: Модульный инфраструктурный плагин с идемпотентными миграциями dbDelta и PSR-4 архитектурой.
* Version: 2.1.0
* Requires at least: 6.7
* Requires PHP: 8.4
* Requires Plugins:
* Author: Artur Yusupov (Fullstack Agentic Lab)
* License: GPL-2.0-or-later
* Text Domain: agentic-lab-core
*/
declare(strict_types=1);
namespace AgenticLab\Plugin;
if (!defined('ABSPATH')) {
exit;
}
final class PluginBootstrap
{
public const VERSION = '2.1.0';
public const DB_VERSION_OPTION = 'agentic_lab_db_schema_version';
public const CRON_HOOK = 'agentic_lab_hourly_telemetry_cleanup';
public static function register(): void
{
// 1. Регистрация PSR-4 автозагрузчика классов
spl_autoload_register(self::autoloadClass(...));
// 2. Хуки жизненного цикла активации и деактивации
register_activation_hook(__FILE__, self::onActivate(...));
register_deactivation_hook(__FILE__, self::onDeactivate(...));
// 3. Проверка обновлений схемы БД при Git/CI-CD деплое (где activation_hook не вызывается)
add_action('plugins_loaded', self::maybeUpgradeDatabaseSchema(...), 5);
}
public static function autoloadClass(string $class): void
{
$prefix = 'AgenticLab\\Plugin\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$relativeClass = substr($class, strlen($prefix));
$file = plugin_dir_path(__FILE__) . 'src/' . str_replace('\\', '/', $relativeClass) . '.php';
if (is_readable($file)) {
require_once $file;
}
}
public static function onActivate(bool $networkWide = false): void
{
if (is_multisite() && $networkWide) {
/** @var list<int> $siteIds */
$siteIds = get_sites(['fields' => 'ids']);
foreach ($siteIds as $siteId) {
switch_to_blog((int) $siteId);
self::installSingleSite();
restore_current_blog();
}
return;
}
self::installSingleSite();
}
private static function installSingleSite(): void
{
self::runDbDeltaMigration();
if (!wp_next_scheduled(self::CRON_HOOK)) {
wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
}
flush_rewrite_rules(false);
}
public static function maybeUpgradeDatabaseSchema(): void
{
$installedVersion = (string) get_option(self::DB_VERSION_OPTION, '0.0.0');
if (version_compare($installedVersion, self::VERSION, '<')) {
self::runDbDeltaMigration();
}
}
/**
* Идемпотентная миграция кастомной таблицы с соблюдением строгих правил парсера dbDelta().
*/
public static function runDbDeltaMigration(): void
{
global $wpdb;
$tableName = $wpdb->prefix . 'agentic_telemetry_logs';
$charsetCollate = $wpdb->get_charset_collate();
// ВАЖНО: 2 пробела после PRIMARY KEY и отсутствие обратных кавычек для корректного диффа dbDelta!
$sql = "CREATE TABLE {$tableName} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
event_type varchar(64) NOT NULL,
payload longtext NOT NULL,
execution_ms float unsigned NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY event_created_idx (event_type, created_at)
) {$charsetCollate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option(self::DB_VERSION_OPTION, self::VERSION, true);
}
/**
* Мягкая деактивация: снимаем только задачи WP-Cron, НЕ удаляя таблицы и настройки пользователя!
*/
public static function onDeactivate(): void
{
wp_clear_scheduled_hook(self::CRON_HOOK);
flush_rewrite_rules(false);
}
}
PluginBootstrap::register();
/*
* ============================================================================
* Изолированный файл деинсталляции: wp-content/plugins/agentic-lab-core/uninstall.php
* (Вызывается ядром автоматически только при полном удалении плагина)
* ============================================================================
*
* <?php
* declare(strict_types=1);
*
* if (!defined('WP_UNINSTALL_PLUGIN')) {
* exit;
* }
*
* global $wpdb;
* $tableName = $wpdb->prefix . 'agentic_telemetry_logs';
* $wpdb->query("DROP TABLE IF EXISTS {$tableName}");
*
* delete_option('agentic_lab_db_schema_version');
* wp_cache_flush();
*/Почему вызов DROP TABLE и удаление опций в хуке register_deactivation_hook() является критической архитектурной ошибкой?
В чем суть ошибки: Вызов DROP TABLE или delete_option() внутри обработчика деактивации плагина (register_deactivation_hook).
Почему разработчики так делают: Смешивание понятий «выключить плагин» и «удалить плагин с сервера».
Что происходит в продакшене: Администратор временно отключает плагин на 30 секунд для диагностики конфликта или обновления версии и навсегда теряет всю базу данных заказов, настроек и логов.
Как правильно: В register_deactivation_hook разрешается только отменять периодические задачи (wp_clear_scheduled_hook) и сбрасывать транзиентный кэш. Полное удаление таблиц и опций выполняется исключительно в файле uninstall.php.
В чем суть ошибки: Размещение вызова dbDelta() исключительно в колбэке register_activation_hook(__FILE__, ...).
Почему разработчики так делают: Предположение, что при обновлении плагина до новой версии хук активации сработает повторно.
Что происходит в продакшене: При автоматическом обновлении плагина из репозитория, через Composer или CI/CD пайплайн WordPress НЕ вызывает register_activation_hook. Новый PHP-код начинает обращаться к новым колонкам таблицы, которых еще нет в MySQL, вызывая каскад фатальных SQL-ошибок на продакшене.
Как правильно: Хранить номер версии схемы в опции (agentic_lab_db_schema_version) и сверять его с константой self::VERSION на хуке plugins_loaded, автоматически запуская dbDelta() при расхождении версий.
В чем суть ошибки: Использование CREATE TABLE IF NOT EXISTS, одинарного пробела в PRIMARY KEY (id) или ключевого слова INDEX вместо KEY при передаче SQL-строки в dbDelta().
Почему разработчики так делают: Привычка к стандартному синтаксису MySQL, который работает в $wpdb->query(), но ломает регулярные выражения парсера dbDelta.
Что происходит в продакшене: При первом запуске таблица может создаться, но при выходе обновления плагина с новой колонкой dbDelta() не сможет корректно распарсить структуру и проигнорирует добавление новых полей и индексов.
Как правильно: Строго соблюдать формат dbDelta: ровно два пробела в PRIMARY KEY (id), ключевое слово KEY для индексов, каждое поле на новой строке и отсутствие IF NOT EXISTS.
В чем суть ошибки: Написание SQL-запросов DROP TABLE в файле uninstall.php без проверки if (!defined('WP_UNINSTALL_PLUGIN')) exit; в первой строке.
Почему разработчики так делают: Забывают, что файл uninstall.php лежит в публично доступной директории /wp-content/plugins/my-plugin/uninstall.php.
Что происходит в продакшене: Хотя без загрузки WordPress объект $wpdb не инициализирован (и скрипт упадет с Fatal Error, раскрыв полный путь на сервере), при определенных конфигурациях автозагрузки прямой HTTP GET-запрос к файлу от бота или злоумышленника может спровоцировать несанкционированный запуск деструктивной логики.
Как правильно: Первой исполняемой инструкцией в uninstall.php всегда должна быть проверка if (!defined('WP_UNINSTALL_PLUGIN')) { exit; }.