Разработка расширений
Как устроено расширение Lil_CMS: манифест, плагин, события, миграции и установка.
Правила, из которых всё следует
- Ядро не знает о расширениях. Расширение подписывается на события ядра, а не правит его файлы. Обновление системы не должно ломать вашу работу.
- Расширение самодостаточно. Свои классы, шаблоны, строки языка и миграции лежат внутри его каталога.
- Строки интерфейса — только через переводчик. Зашитый в код текст нельзя перевести, а язык в системе выбирает владелец сайта.
- Права проверяются в диспетчере. Действие без объявленного права недоступно никому — так безопаснее, чем «разрешено, пока не запретили».
Полный текст этих правил и примеры лежат в файле docs/EXTENSIONS.md внутри самой системы — на сайте собрано главное.
Каталог расширения
Вид расширения виден по приставке имени: com_ — компонент, mod_ — модуль, plg_ — плагин. Тема лежит своим каталогом.
extensions/
components/com_shop/ компонент магазина
lil.json манифест
src/ классы
templates/ шаблоны экранов и витрины
language/ строки ru-RU.ini, en-GB.ini
migrations/ таблицы расширения
modules/mod_menu/ модуль меню сайта
plugins/shop/plg_shop_discounts/ плагин скидок
themes/lil_default/ тема сайтаФайл lil.json
Манифест описывает расширение: имя, версию, автозагрузку классов, требования и настройки. Без него система расширение не увидит.
{
"type": "component",
"element": "com_example",
"name": "Пример",
"version": "0.0.00001",
"description": "Что делает расширение — одной фразой.",
"author": "Ваше имя",
"license": "GPL-2.0-or-later",
"provider": "Lil\\Component\\Example\\ExampleServiceProvider",
"autoload": {
"Lil\\Component\\Example\\": "src"
},
"requires": {
"core": "0.0.00300",
"php": "8.3"
},
"migrations": "migrations",
"templates": "templates",
"language": "language"
}Версия растёт на единицу при каждой правке — тем же правилом, что и у самой системы. requires.core — минимальная версия ядра: без неё клиент не проверит совместимость перед установкой.
Плагин: подписка на события
Плагин ничего не знает о ядре, кроме имени события, и ядро ничего не знает о плагине. Так исправление в ядре не ломает плагин, а плагин не ломает сайт.
final class ExamplePlugin extends Plugin
{
public function subscribe(EventBus $events): void
{
// Приоритет ниже нуля: сначала отработают те, кто собирает
// страницу, и только потом мы правим готовый результат.
$events->listen(ResponseEvent::class, $this->touch(...), -100);
}
private function touch(ResponseEvent $event): void
{
$response = $event->response();
// Правим только HTML: подставлять текст в ответ API
// значило бы сломать его для того, кто его разбирает.
if (!str_contains((string) $response->header('Content-Type'), 'text/html')) {
return;
}
// Response неизменяем: в событие кладётся новый объект,
// правка «на месте» потерялась бы молча.
$event->setResponse(new Response($body, $response->status()));
}
}События ядра
Шина событий типизирована: подписка идёт на класс события, а не на строку — опечатка в имени станет ошибкой сразу, а не молчанием в работе.
ResponseEvent— готовый ответ перед отправкой браузеру.CollectBlocks— сбор блоков конструктора страниц: так компонент добавляет свой блок.ShopPriceEvent— цена товара: витрина, корзина и заказ считают одинаково, поэтому скидки живут здесь.CollectPaymentMethods,CollectShippingMethods— способы оплаты и доставки магазина.
Приоритет задаёт порядок: чем меньше число, тем позже вызов. Обработчик может прервать цепочку, если событие это допускает.
Свои таблицы
Каждая миграция обязана уметь откатываться: метод down() возвращает базу в прежнее состояние. Это не формальность — на откате держится восстановление после неудачного обновления.
return new class () extends Migration {
public function description(): string
{
return 'Пример: своя таблица';
}
public function up(Schema $schema): void
{
$schema->create('example_items', static function (Blueprint $table): void {
$table->id();
$table->string('title', 190)->nullable(false);
$table->timestamps();
});
}
public function down(Schema $schema): void
{
$schema->drop('example_items');
}
};Установка и жизненный цикл
php bin/lil extension:discover найти расширения на диске
php bin/lil extension:install com_example --enable
php bin/lil extension:disable com_example
php bin/lil extension:uninstall com_example
php bin/lil version:bump com_example поднять версиюУстановленное расширение включается отдельным действием: расширение, включающееся само по факту установки, — это способ получить работающий чужой код раньше, чем владелец сайта успел на него взглянуть.
Удаление снимает миграции расширения и убирает запись из реестра. Файлы остаются на диске — их удаляет тот, кто их туда положил.
Что проверяется перед выпуском
- Весь вывод экранирован; сырой HTML — только после очистителя и по праву.
- Изменяющие запросы проходят проверку токена формы.
- Запросы к базе — подготовленными выражениями, без склейки строк.
- Право на действие объявлено и проверяется в диспетчере.
- Строки интерфейса переведены на русский и английский.
- Ошибки не раскрывают путей, запросов и версий.
Готовое расширение собирается в подписанный пакет на сервере обновлений и попадает в каталог расширений — оттуда его ставят на другие сайты одной кнопкой.