Тема
Фаза 0 — Контракт модуля
Статус: спека фазы 0. Что обязан и что не смеет делать модуль. Разворачивает «7 пунктов» из расширяемости в проверяемый контракт. Стыкуется с жизненным циклом модулей (состояния, версии, откат — там; здесь — структура и границы), с моделью данных (владение данными) и безопасностью (capability-слой).
Что такое модуль
Модуль — приватный composer-пакет, добавляющий возможность, нужную некоторым сайтам. Он не форкает ядро: всё, что делает, — регистрирует через открытые точки ядра и реагирует на его события. Форк ядра или прямая запись в его таблицы — нарушение контракта, а не «продвинутое использование».
Контракт — это набор из обязательств (модуль ДОЛЖЕН), запретов (модуль НЕ СМЕЕТ) и гарантий ядра (ядро ОБЕЩАЕТ модулю). Проверяется пакетом cms-testing — модуль без зелёного контрактного прогона не публикуется в Satis.
Структура пакета
cms-catalog/
├── composer.json # require + extra.cms (манифест, см. ниже)
├── src/
│ ├── CatalogServiceProvider.php # единственная точка входа модуля
│ ├── Blocks/ # типы блоков (BlockType-описания)
│ ├── Widgets/ # типы виджетов
│ ├── Filament/ # ресурсы/страницы (Filament-плагин)
│ ├── Lifecycle/ # Install / Enable / Disable / Uninstall
│ ├── Listeners/ # реакции на события ядра
│ ├── Policies/ # свои политики (не переопределяют ядро)
│ └── Contracts/ # если модуль сам — точка расширения для других
├── database/migrations/ # только таблицы модуля (префикс catalog_)
├── content/ # переносимый демо-контент (см. модель данных)
├── routes/ # роут-файлы, регистрируются через хук ядра
├── resources/views/ # blade модуля (namespace = имя модуля)
├── lang/ # строки UI модуля (через __())
└── tests/ # + прогон контрактных тестов cms-testingОдин provider — единственная точка входа. Всё остальное он регистрирует, а не ядро его сканирует (никакого директорного PluginManager-скана — штатное composer auto-discovery).
Манифест модуля
Живёт в composer.json → extra.cms. Полная форма (поля жизненного цикла описаны в спеке жизненного цикла):
jsonc
{
"name": "cms/catalog",
"extra": {
"cms": {
"schema_version": 7,
"minimum_core_version": "2.3.0",
"requires": [],
"suggests": ["cms/search"],
"conflicts": [],
"provides": [],
"load_after":["cms/seo"],
"enables": [],
"lifecycle": { "install": "...", "enable": "...", "disable": "...", "uninstall": "..." },
"health": ["Cms\\Catalog\\Health\\CatalogCheck"],
"permissions": ["catalog.view", "catalog.manage"],
"capabilities": ["own-tables", "own-routes", "own-events", "register-blocks"]
}
}
}capabilities — явный список того, что модуль намерен делать (см. capability-слой ниже). Ядро сверяет фактические действия модуля с декларацией: модуль, пытающийся сделать недекларированное (напр. писать в users), останавливается.
Обязательства модуля (модуль ДОЛЖЕН)
1. Регистрировать вклад в boot(), уважая состояние
php
final class CatalogServiceProvider extends ServiceProvider
{
public function register(): void
{
// ПЕРВЫМ делом — проверка состояния: выключенный модуль не тратит ресурсы
if (! ModuleState::isEnabled('cms/catalog')) {
return;
}
// биндинги контейнера
}
public function boot(): void
{
if (! ModuleState::isEnabled('cms/catalog')) {
return;
}
BlockRegistry::register(CatalogGridBlock::describe());
WidgetRegistry::register(CatalogFilterWidget::describe());
$this->registerRoutesVia(CoreRouter::class); // НЕ Route:: напрямую
Filament::registerPlugin(CatalogPlugin::make());
$this->registerListeners();
}
}2. Привозить свои миграции — только свои таблицы
- префикс имени модуля (
catalog_products,catalog_categories); down()обязателен (условие отката из жизненного цикла);- additive/expand на минорах; ломающее изменение схемы = мажор + план expand-contract;
- FK на таблицы ядра допустимы, но модуль не меняет схему ядра (только читает через контракт).
3. Регистрировать роуты через хук ядра, не Route:: напрямую
Прямой вызов Route:: в провайдере несовместим с route:cache — это зафиксированный инцидент TrailingSlashUrlGenerator в universal. Модуль отдаёт роут-файл ядру:
php
$this->registerRoutesVia(function (CoreRouter $r) {
$r->group(['prefix' => 'catalog', 'middleware' => ['web', 'cms.city']], base_path('...routes/web.php'));
});Ядро собирает все роуты модулей в один кешируемый набор с корректным UrlGenerator.
4. Поставлять Filament-плагин, а не лезть в чужую панель
Ресурсы/страницы/виджеты модуля — через FilamentPlugin. Регистрация в общую панель, свои permissions декларируются (Shield подхватывает). Модуль не редактирует ресурсы ядра или других модулей — только добавляет свои (или расширяет через события Filament render-hooks).
5. Реагировать на события ядра, а не патчить его код
Лид создан, страница сохранена, кеш сброшен — модуль слушает (события — ось 1 из трёх):
php
#[CmsListener(event: PageSaved::class, after: [SeoListener::class])]
final class WarmCatalogCache { public function handle(PageSaved $e): void { /* ... */ } }Никаких «monkey-patch ядра», глобальных do_action, правок vendor/cms/core.
6. Декларировать совместимость и права
require: {"cms/core-contracts": "^2.0"}— зависимость от контрактов, не реализации;minimum_core_versionв манифесте (человекочитаемая ошибка в UI);permissions[]— Shield-права модуля (обязательно, если у модуля есть админ-действия);capabilities[]— что модуль намерен делать (сверяется ядром).
7. Декларировать настройки, health-чеки и документацию
- настройки — только через
SettingSchema(ось 3): свой стор/таблица настроек запрещены; десятки настроек — норма, но все читаются через кеш группы; - health-чеки — поле
healthманифеста (самодиагностика); - документация — модуль несёт
CLAUDE.md(для агента) иdocs/module.md: что делает, схема данных, точки расширения, зависимости и обратные зависимости, какие настройки/события/права декларирует, что произойдёт при выключении. Изdocs/module.mdсобирается страница модуля в docs-site — по одному MD-файлу на модуль, источник живёт в пакете рядом с кодом (DX).
8. Проходить контрактные тесты cms-testing
Модуль подключает набор проверок и держит его зелёным (см. ниже). Красный контракт = не публикуется.
Запреты модуля (модуль НЕ СМЕЕТ)
Из «модуль ≠ ядро по правам» и владения данными:
| Запрет | Почему | Что вместо |
|---|---|---|
Писать в таблицы ядра (cms_*) напрямую | Ядро свободно меняет схему → сломает модуль | Контракт cms/core-contracts (репозитории/сервисы) |
Читать схему ядра (SQL к cms_pages и т.п.) | Привязка к внутренней структуре | Те же контракты + события |
| Менять схему ядра миграцией | Обновление ядра затрёт | Свои таблицы + полиморфные точки (cms_seo_meta) |
| Переопределять политики/права ядра | Тихая эскалация привилегий | Только добавлять свои Policy/permissions |
Route:: напрямую в провайдере | Ломает route:cache (инцидент universal) | Хук CoreRouter |
| Трогать пользователей/роли ядра | Обход RBAC | Свои сущности + связь по FK |
env() в коде | Ломается при config:cache | .env → config() |
{!! $userInput !!} в blade | XSS (91% CVE WP — в плагинах) | ; линтер запрещает вне whitelist |
Capability-слой (песочница прав)
Явная модель того, что модулю разрешено, — заготовка под открытый маркетплейс, которую нельзя ретрофитить потом.
| Capability | Даёт право | Проверка ядром |
|---|---|---|
own-tables | создавать/мигрировать таблицы своего префикса | миграции вне префикса — отклоняются |
own-routes | регистрировать роуты через CoreRouter | прямой Route:: — линтер контракта |
own-events | публиковать свои события, слушать события ядра | — |
register-blocks | добавлять типы в BlockRegistry/WidgetRegistry | — |
filament-plugin | добавлять ресурсы в панель | правка чужих ресурсов — запрещена |
outbound-http | ходить во внешние API (интеграции) | через integration-core (креды/ретраи/логи) |
Модуль без декларации capability на действие — не может это действие выполнить. Сейчас (закрытый маркетплейс) проверки мягкие (предупреждение в CI/cms:doctor); при открытии маркетплейса (фаза 5) — жёсткий enforcement. Модель уже на месте.
Гарантии ядра (ядро ОБЕЩАЕТ модулю)
Контракт двусторонний. Ядро обязуется:
- стабильность
cms/core-contracts— публичные интерфейсы/события меняются только по semver с deprecation-циклом (≥1 minor, модель Symfony); - изоляция сбоев — исключение в
boot()/рендере/слушателе модуля не роняет ядро и соседей (жизненный цикл); - порядок загрузки по
load_after(топологическая сортировка); - единые оси — модуль не изобретает свой стор настроек / очередь / шину событий, а пользуется тремя осями ядра;
- реестр точек расширения —
php artisan cms:hooksпоказывает все события/фильтры с типами payload, чтобы модуль знал, куда вклиниваться.
Модуль как точка расширения (модуль для модулей)
Модуль может сам быть основой для других (provides): например, cms/payments объявляет контракт payment-gateway, а cms/payment-yookassa его реализует. Тогда модуль-основа:
- кладёт свой контракт в
src/Contracts/и публикует его как под-пакет*-contracts; - регистрирует свой реестр реализаций (аналог BlockRegistry);
- объявляет
provides: ["payment-gateway"], реализации —requiresна него.
Так лестница расширяемости работает не только «ядро → модуль», но и «модуль → под-модуль» без изменения контракта.
Контрактные тесты (cms-testing)
Набор, который модуль обязан держать зелёным:
- манифест валиден — все обязательные поля,
requires/conflictsразрешимы, semver корректен; - миграции обратимы —
up()затемdown()не оставляют следов; префикс соблюдён; - блоки/виджеты проходят контракт блока — demo валиден, цепочки
_vзамкнуты, нет стилевых полей; - роуты кешируемы —
route:cacheне падает (регистрация черезCoreRouter); - нет запрещённых действий — статический анализ: нет записи в
cms_*, нетRoute::, нетenv(), нет{!! !!}вне whitelist; - capability соблюдены — фактические действия ⊆ декларированных
capabilities; - lifecycle идемпотентен —
install→uninstall→installчист;enable/disableобратимы; - события изолированы — брошенное в слушателе исключение не ломает цепочку.
Чеклист фазы 0 (контракт модуля)
- [ ] Структура пакета + единственный service provider со state-guard.
- [ ] Манифест
extra.cmsc requires/provides/lifecycle/health/permissions/capabilities. - [ ]
docs/module.md+CLAUDE.md— обязательная документация модуля (файл на модуль). - [ ] Регистрация роутов через
CoreRouter(неRoute::) — защитаroute:cache. - [ ] Filament-плагин; модуль не правит чужие ресурсы.
- [ ] Capability-слой: декларация + сверка ядром (мягкая сейчас, жёсткая в фазе 5).
- [ ] Запреты (таблицы ядра, схема ядра, права, env, XSS) — статическим анализом.
- [ ] Двусторонние гарантии ядра (стабильность контрактов, изоляция, порядок, оси).
- [ ] Контрактные тесты
cms-testingзелёные — условие публикации в Satis.
Связи
- Расширяемость → контракт модуля — исходные 7 пунктов.
- Жизненный цикл модулей — состояния, версии, откат, ошибки.
- Зависимости модулей — граф requires/provides/load_after.
- Модель данных — владение данными.
- Безопасность — capability и «модуль ≠ ядро».
- Три оси — события/очереди/настройки, которыми пользуется модуль.