Структура модуля
Модуль — самодостатня тека в app/Modules/<Name>/:
app/Modules/MyModule/
├── module_info.php # метадані: назва, версія, опис (єдине джерело версії)
├── Config/
│ ├── Routes.php # роути модуля
│ ├── Events.php # підписки на події ядра, точки рендеру (опційно)
│ └── Registrar.php # внески в реєстри ядра: меню, CSRF, планувальник... (опційно)
├── Controllers/
│ ├── MyModule.php # публічні контролери (storefront)
│ └── Admin/
│ └── MyModuleAdmin.php# адмін-контролери
├── Models/ # CI4-моделі модуля
├── Libraries/ # бізнес-логіка, install()/ensureSchema() для власних таблиць
│ └── ModuleUpdater.php # самооновлення (типовий компонент)
├── Commands/ # spark-команди (планові задачі, обслуговування)
├── Resources/
│ └── install.sql # опційний ідемпотентний SQL, який виконує store-інсталятор
├── Views/
│ └── admin/ # Twig-в'юхи адмінки
└── .baseline.json # маніфест файлів версії (генерується при збірці)
module_info.php
Єдине джерело правди про версію модуля. Його читають самооновлювач і сервер оновлень; кожен реліз має піднімати версію тут.
Як ядро бачить модуль
Auto-discovery CI4 підхоплює Config/Routes.php, Config/Events.php і
Config/Registrar.php лише для відомих йому просторів імен. Тому вбудованому
модулю потрібен один рядок у $psr4 файлу app/Config/Autoload.php
('App\Modules\MyModule' => APPPATH . 'Modules/MyModule'); без нього роути
404-ять у slug-фолбек, а події мовчать — composer dump-autoload не
достатньо. Модулі з магазину цей файл не чіпають: інсталятор пише простір імен
у writable/store_modules.json, який Config\Autoload зливає при
завантаженні.
Модулю також потрібен рядок у таблиці components (identif, enabled,
active); store-інсталятор створює його сам, вбудований модуль — у своєму
кроці встановлення.
Доступ до даних
Нові модулі використовують лише Query Builder CI4
(\Config\Database::connect()) або CodeIgniter\Model — без Propel
(App\Propel\*Query::create(), joinWithI18n, Propel-моделі) у новому коді.
Зразки, написані так: CloudStorage, Comments/Controllers/CommentsApi.php.
Модуль володіє своєю схемою: таблиці створюються в ідемпотентному
install()/ensureSchema() (CREATE TABLE IF NOT EXISTS, INSERT IGNORE для
початкових рядків), який викликається при встановленні або ліниво перед першим
використанням. Модулі не везуть міграцій CI4; міграції ядра — лише для схеми
ядра (і spark migrate на живому сайті безпечний — baseline має гвард).
Config/Registrar.php — внески в реєстри ядра
Замість правок app/Config/* модуль оголошує потрібне у статичному методі
ModuleExtensions(); registrar auto-discovery CI4 зливає його в
Config\ModuleExtensions:
namespace App\Modules\MyModule\Config;
class Registrar
{
public static function ModuleExtensions(): array
{
return [
'csrfExempt' => ['my_module/callback'], // URI-префікси без CSRF
'lsCacheNever' => ['my_module/exchange'], // ніколи не кешувати сторінку
'adminMenu' => [['section' => 'settings', 'item' => [
'identifier' => 'my_module', 'text' => cms_lang('My module', 'admin_menu'),
'link' => '/admin/my-module', 'class' => '', 'id' => '', 'pjax' => '', 'icon' => '',
]]],
'jobHandlers' => ['my_module:sync' => [Handler::class, 'run']],
'scheduledTasks' => ['my-module-sync' => [
'command' => 'my_module:sync', 'every' => 900, // або 'at' => '04:10'
'label' => 'Мій модуль: синхронізація', 'module' => 'my_module', 'enabled' => true,
]],
'adminTranslations' => ['my_module' => ['Save' => ['uk_UA' => 'Зберегти', 'ru_RU' => 'Сохранить', 'en_US' => 'Save']]],
];
}
}
Підтримувані ключі: csrfExempt, lsCacheNever, moduleEnabledAlways
(машинні ендпоїнти, які не мають 404-ити при вимкненому модулі), adminMenu,
jobHandlers, scheduledTasks, adminTranslations.
Планові задачі
Запис scheduledTasks — це spark-команда (Commands/) плюс every (секунди)
або at (HH:MM щодня). Внутрішній планувальник запускає її окремим
підпроцесом і показує в Налаштування → Планувальник (/admin/scheduler),
де власник може вимкнути її або виконати зараз. Задача доступна, лише коли
module має активний рядок у components і команда є в реєстрі spark —
перейменування команди без оновлення запису мовчки паркує задачу. Див.
Планувальник.
Правила інтеграції з ядром
Якщо модуль реєструє клас у конфігурації ядра (фільтр у Config/Filters.php,
роут-хук тощо), його видалення або оновлення без цього класу покладе весь
сайт — ядро спробує завантажити неіснуючий клас на кожному запиті.
Правильний шлях: інтегруватися через події (Config/Events.php всередині
модуля) та auto-discovery простору імен. Тоді вимкнений чи видалений
модуль просто перестає працювати, не ламаючи систему.
- У кожному слухачі події — перевірка, чи модуль увімкнено
(
module_enabled('my_module')), іtry/catch: проблема модуля не має зривати бізнес-операцію ядра. - Віджети вітрини додаються через точки рендеру
(слухачі
storefront_render_point:<name>уConfig/Events.php), а не записом у файли теми. - Якщо ядру бракує потрібного шва — додайте в ядро generic-подію чи ключ реєстру, а не виклик класу вашого модуля.
- Файли модуля належать власнику сайту; після деплою руками не забувайте про права та скидання opcache.
Адмін-контролери
Адмінські екрани модуля рендеряться через Twig-в'юхи модуля
(Views/admin/*.twig) у стилі адмін-теми (admin-redesign, rd-* класи).
Не передавайте SimpleXMLElement напряму у в'юху — Twig падає на його
ітераторі. Конвертуйте у масив:
json_decode(json_encode($xml), true).
Самооновлення
Типовий модуль містить Libraries/ModuleUpdater.php і адмін-дії
check_update/do_update: перевірка версії на сервері оновлень, завантаження
пакета, бекап поточної версії, розпаковка, звірка з .baseline.json.