Архітектура сучасного корпоративного плагіна у 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 $\neq$ Deactivation $\neq$ 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 та очищення транзієнтного кешу з повним збереженням даних клієнта.
Ізольований запуск скрипту очищення при видаленні плагіна: видалення кастомних таблиць, опцій та мета-ключів.
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 event_created_idx (event_type, 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 або 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; }.
Ядро 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 та повне очищення таблиць, опцій та мета-полів плагіна.
Виконується тільки тоді, коли адміністратор явно видаляє неактивний плагін.