WordPress завантажує плагіни до теми й для будь-якої теми, а functions.php читає тільки з активної теми (і з батьківської, якщо активна дочірня). Звідси й весь розподіл: код у плагіні переживає редизайн, код у темі вимикається в ту секунду, коли хтось натиснув «Активувати» на іншій темі. Кордон проходить приблизно так: тема описує, як контент виглядає, плагін - що це за контент і що з ним відбувається.
Найболючіший сценарій це register_post_type у темі. Після зміни теми клієнт бачить, що з адмінки пропав розділ «Товари», сторінки товарів віддають 404, а на головній порожньо. У базі при цьому все ціле: рядки в wp_posts з post_type = 'shop_product', метадані, зв'язки з термінами. Просто в $wp_post_types більше немає такого типу, тому адмінка не малює меню, WP_Query за публічним запитом його ігнорує, а rewrite-правила не знають про /products/. Лікується поверненням реєстрації в плагін, без відкату бекапу. Так само поводяться шорткоди: контент статей зберігає [product_price id="12"] як текст, і без add_shortcode читач бачить саму дужку.
Окремо стоїть код, який не має бути вимкненим навіть випадково, і тут працюють mu-plugins. Файли з wp-content/mu-plugins підключаються раніше за звичайні плагіни й не мають кнопки деактивації в адмінці. Туди кладуть примусову конфігурацію пошти, заборону редактора файлів, мережеві налаштування Multisite. Обмежень два, і про них питають: хуків активації в mu-plugins не існує взагалі, а сканується лише перший рівень теки, тому плагін із власною структурою підключають однорядковим loader-файлом із require.
Хуки життєвого циклу часто розуміють навпаки. register_activation_hook виконується один раз при активації: там доречні dbDelta для власних таблиць, add_option з версією схеми, add_role, wp_schedule_event і flush_rewrite_rules(), але саме після виклику функції реєстрації CPT, бо інакше правила перебудуються без знання про новий тип. Реєструвати в цьому хуку add_action безглуздо: до наступного запиту від нього не лишиться нічого. register_deactivation_hook симетрично прибирає розписання й кеші, але не дані: плагін вимикають і для діагностики, і втратити каталог через це неприпустимо. Видалення користувацьких даних живе в uninstall.php, який WordPress виконує окремим запитом і без завантаження самого плагіна, тому перший рядок там це перевірка defined('WP_UNINSTALL_PLUGIN').
Мінімальна структура невеликого плагіна: головний файл із докблоком Plugin Name (без нього WordPress плагіна не побачить), inc/ або src/ із класами, uninstall.php, readme.txt за потреби публікації, languages/ для перекладів. Один файл на 200 рядків цілком нормальний плагін; ділити на класи варто тоді, коли з'являються дві незалежні відповідальності. Composer з автозавантаженням у WordPress-плагіні цілком доречний, але префіксуйте залежності (Mozart, PHP-Scoper): у середовищі, де паралельно живуть десятки плагінів, конфлікт версій однієї бібліотеки трапляється регулярно.
<?php
/**
* Plugin Name: Shop Catalog
* Version: 1.0.0
*/
// Логіка даних живе в плагіні: тема може змінитись, тип запису лишиться
add_action('init', 'shop_register_product_type');
function shop_register_product_type(): void
{
register_post_type('shop_product', [
'labels' => ['name' => 'Товари'],
'public' => true,
'has_archive' => true,
'rewrite' => ['slug' => 'products'],
'show_in_rest' => true, // потрібно для блокового редактора
]);
}
// Шорткод теж у плагіні, інакше старі статті віддадуть сирий текст
add_shortcode('product_price', function (array $atts): string {
$id = (int) ($atts['id'] ?? 0);
return esc_html((string) get_post_meta($id, '_price', true));
});
// Активація: одноразові дії, жодних add_action тут
register_activation_hook(__FILE__, function (): void {
shop_register_product_type(); // спершу реєструємо тип...
flush_rewrite_rules(); // ...тоді перебудовуємо правила URL
add_option('shop_schema_version', '1.0.0');
});
// Деактивація: знімаємо розписання, дані користувача не чіпаємо
register_deactivation_hook(__FILE__, function (): void {
wp_clear_scheduled_hook('shop_sync_stock');
flush_rewrite_rules();
});
Сформулюйте правило одним реченням: «якщо після зміни теми це має продовжити працювати - це плагін». Додайте, що деактивація не має видаляти дані, а видалення користувацьких таблиць чи опцій - робота uninstall.php, а не деактиваційного хука.