Тема
Фаза 0 — Жизненный цикл и надёжность модулей
Статус: спека фазы 0. Закрывает рантайм-надёжность, которую граф зависимостей и центр обновлений подразумевали, но не описывали: установка/включение/откат модуля, поведение при рассогласовании версий (старый блок ↔ новый модуль, модуль выключен), и — главное — как это видит человек (понятные ошибки, а не 500). Опирается на зависимости модулей, центр обновлений, контракт блока, модель данных.
Главный инвариант: деградация, а не падение
Правило, которого не было и которое определяет весь дизайн: рассогласование версий никогда не роняет публичную страницу. Отсутствующий модуль, устаревший блок, несовместимая версия — всё это переводит систему в явно деградированное, видимое администратору состояние, но публичный сайт остаётся живым. Урок Битрикс/WP: «белый экран смерти» при конфликте модуля недопустим в managed-парке, где падение видит клиент, а чинит студия постфактум.
Три следствия этого инварианта проходят через всю спеку:
- рендер-путь блока/виджета от модуля обёрнут в безопасный барьер (fallback вместо исключения);
- любое несоответствие версий обнаруживается заранее (preflight), а не в проде;
- каждое деградированное состояние имеет UI-представление в админке и в телеметрии.
Версионирование модуля
Модуль версионируется по semver, и версия имеет три независимых грани — их смешение и есть корень «непонятных ошибок при обновлении».
| Грань | Что версионирует | Где живёт | Кто ломается при рассинхроне |
|---|---|---|---|
| Версия пакета | код модуля | composer.json, composer.lock | composer (ловится до установки) |
| Версия схемы БД | таблицы модуля | миграции + cms_module_versions | данные (ловится migrate/health-check) |
Версия схемы блока (_v) | форма props блоков модуля | запись блока в JSONB + BlockRegistry | контент (ловится при рендере/migrate) |
Правило соответствия (semver с доменным смыслом):
- patch — только код (баг-фикс), схема БД и
_vне меняются; - minor — аддитивно: новое поле блока с дефолтом, новая nullable-колонка, новый блок.
_vможет вырасти, но старые записи валидны без миграции (expand-only); - major — ломающее: удаление/переименование поля, смена типа, удаление блока/события. Обязательны: класс data-миграции блока, expand-contract план по БД, запись в CHANGELOG, bump
minimum-core-versionпри необходимости.
Совместимость с ядром — единая формула (та же в центре обновлений): в composer модуль зависит от пакета контрактов — require: {"cms/core-contracts": "^2.0"}, а не от реализации ядра (внутренний рефакторинг ядра не считается breaking для модулей); совместимость с реализацией декларируется minimum-core-version в манифесте — проверяется preflight'ом, даёт человекочитаемую ошибку в UI.
Манифест модуля (полный)
Расширяет декларации из зависимостей полями жизненного цикла:
jsonc
// composer.json → extra.cms
{
"name": "cms/catalog",
"schema_version": 7, // версия схемы БД модуля
"minimum_core_version": "2.3.0", // человекочитаемая нижняя граница ядра
"requires": [], // только МОДУЛИ; подсистемы ядра (движок полей и т.п.) доступны всегда
"suggests": ["cms/search"],
"conflicts": [],
"provides": [],
"load_after":["cms/seo"],
"enables": [], // каскад включения (метапакет)
"lifecycle": { // хуки жизненного цикла (классы)
"install": "Cms\\Catalog\\Lifecycle\\Install",
"enable": "Cms\\Catalog\\Lifecycle\\Enable",
"disable": "Cms\\Catalog\\Lifecycle\\Disable",
"uninstall": "Cms\\Catalog\\Lifecycle\\Uninstall"
},
"health": ["Cms\\Catalog\\Health\\CatalogCheck"], // самодиагностика (см. ниже)
"permissions": ["catalog.view", "catalog.manage"] // Shield подхватит
}Состояния модуля (машина состояний)
Модуль на сайте живёт в одном из состояний. Переходы — только через команды ядра, каждая проверяет граф зависимостей и пишет в cms_module_history.
(нет пакета)
│ composer require + cms:module:install
▼
installed ──enable──▶ enabled ──disable──▶ disabled
▲ │ │
│ │ (код грузится, │ (код в vendor, но
│ │ роуты/блоки/ │ провайдер не boot-ится,
│ │ события активны) │ роуты/блоки/события сняты)
└──uninstall◀────────┴─────────────────────┘
(down-миграции, чистка; данные — по политике: keep|purge)- installed — код в
vendor/, миграции применены, но модуль не активен (провайдер зарегистрирован, ноboot()— no-op). Промежуточное состояние: поставили, но ещё не включили. - enabled — полноценно работает: роуты, блоки в BlockRegistry, слушатели событий, Filament-ресурсы, permissions.
- disabled — код физически в
vendor/, но ничего не регистрирует. Данные модуля в БД сохраняются. Это ключевое состояние для сценария «модуль выключен» (см. ниже). - uninstalled —
down()-миграции, чистка. Данные — по флагу:keep(осиротить, можно вернуть) илиpurge(удалить). По умолчанию keep — необратимое удаление контента клиента требует явного подтверждения.
Реализация состояния — laravel/pennant (feature-flag на модуль) + запись в cms_module_versions.status. Провайдер модуля в своём register() первым делом проверяет флаг и, если не enabled, выходит — ноль накладных расходов на выключенном модуле (принцип «тяжесть = число включённых модулей»).
Установка модуля (пайплайн)
Установка — не одна кнопка, а транзакция с preflight. Каждый шаг может остановить процесс с понятной ошибкой до применения изменений.
cms:module:install cms/catalog
1. composer require --dry-run → конфликт версий? СТОП, показать `composer why-not`
2. проверка minimum_core_version → ядро старее? СТОП: «Каталогу нужно ядро ≥2.3, у вас 2.1»
3. проверка requires (граф) → нет field-engine? предложить включить каскадом
4. проверка conflicts → есть несовместимый? СТОП со списком
5. composer require (реальный) → атомарно, в releases/{ts}
6. bootstrap-preflight (см. ниже) → dry-run загрузки провайдера без активации
7. backup БД → перед миграциями модуля
8. migrate (только миграции модуля) → additive/expand
9. lifecycle.install → сидинг демо-контента, дефолтные настройки
10. запись в cms_module_versions/history
11. состояние → installed (не enabled — включение отдельным шагом/подтверждением)Bootstrap-preflight (шаг 6) — то, чего не хватало: перед активацией ядро в отдельном процессе пробует загрузить провайдер модуля и собрать его вклад (блоки, роуты, permissions) в изолированном контейнере, не публикуя в рабочий. Ловит «модуль ссылается на класс, которого нет в этой версии ядра» до того, как это увидит посетитель.
Откат модуля
Откат — не «удалить», а вернуть к предыдущей рабочей версии, синхронно по всем трём граням версии (иначе рассинхрон, о котором ниже).
cms:module:rollback cms/catalog [--to=6.4.0]
1. определить целевую версию из cms_module_history (предыдущая рабочая)
2. composer require cms/catalog:6.4.0 (или git checkout composer.lock^ + install)
3. schema: migrate:rollback ДО schema_version целевой ← только contract-безопасные шаги
ИЛИ восстановление БД из бэкапа, если миграция была необратимой (data-loss)
4. cms:blocks:migrate --down → откат _v блоков этого модуля (если объявлен обратный)
5. opcache reset + php artisan queue:restart (+ octane:reload при использовании)
6. health-check; при провале — восстановление БД из бэкапа шага install/upgrade
7. запись в history с пометкой ROLLED_BACKИнвариант отката (из центра обновлений): три слоя синхронны — composer.lock + схема БД + _v блоков. Откат одного слоя без остальных = именно тот рассинхрон, что порождает «непонятные ошибки». Поэтому откат — одна команда, а не ручные шаги.
Ограничение честности: не всякий major-откат чист. Если major удалил колонку и стёр данные, migrate:rollback их не вернёт — спасает только бэкап (шаг перед апгрейдом). Это явно сообщается: «Откат Каталога с 7.0 на 6.4 восстановит данные из бэкапа от {дата} — изменения контента после этой даты будут потеряны. Продолжить?».
Рассогласование версий — четыре сценария и поведение
Это ядро вашего вопроса. Для каждого — что происходит, что видит посетитель, что видит администратор.
Сценарий A. Старый блок (_v) ↔ новая версия модуля
Запись блока имеет _v=2, установленный модуль ждёт _v=4.
- Поведение: при рендере
BlockMigratorпрогоняет объявленную цепочку2→3→4(data-миграция props, лениво, с обратной записью в ревизию). Посетитель видит корректный блок. - Если цепочка разорвана (нет миграции для
3→4): рендер отдаёт fallback-заглушку (blocks/_fallback.blade.php: «Блок временно недоступен») + пишет warning в лог, не бросает исключение. Публичная страница цела. - Администратор видит: в списке страниц — бейдж «N блоков требуют миграции»; на странице «Обновления» — «Каталог 7.0: незакрытая миграция блока
catalog-grid3→4». - Профилактика: контрактный тест блока требует замкнутой цепочки
1→…→current— такой разрыв не проходит CI и до прода не доезжает.
Сценарий B. Модуль выключен, а его блок/виджет есть на странице
Контент ссылается на блок catalog-grid, но модуль cms/catalog в состоянии disabled.
- Поведение: блока нет в BlockRegistry (выключенный модуль не регистрирует). Рендер видит неизвестный
type→ отдаёт fallback-заглушку «Блок недоступен: модуль Каталог выключен», не 500. - Посетитель: видит страницу без этого блока (или с нейтральной заглушкой), остальной контент работает.
- Администратор: при попытке выключить модуль, чей блок используется, — предупреждение в момент выключения: «Каталог используется в 12 блоках на 4 страницах. Выключить? Эти блоки станут недоступны». Это обратная проверка использования (materialized
cms_block_usageиз модели данных). - Виджет: аналогично — область просто не рендерит виджет выключенного модуля.
Сценарий C. Модуль требует версию ядра/другого модуля выше установленной
cms/catalog объявляет minimum_core_version: 2.3.0, установлено ядро 2.1.
- Поведение: ловится на install (шаг 2) — модуль не устанавливается, состояние не меняется. Если рассинхрон возник иначе (ручная правка
composer.json) — ловитcms:verifyи boot-preflight: провайдер модуля не активируется, модуль принудительно переводится вdisabledс причиной. - Посетитель: не затронут (модуль не активировался — как в сценарии B).
- Администратор: явная ошибка в UI: «Каталог 7.0 требует ядро ≥ 2.3.0, установлено 2.1.0. Обновите ядро или откатите Каталог до 6.x». С готовыми командами.
Сценарий D. Старый компонент/тема ↔ новый контракт модуля/ядра
Тема или app/Local вызывает метод/событие, которого в новой версии больше нет.
- Защита контрактом: модули и темы зависят от
cms/core-contracts. Удаление публичного метода/события = major ядра + deprecation-цикл (≥1 minor с предупреждением, модель Symfony) — тема успевает мигрировать. - Если всё же вызван удалённый метод: ядро отдаёт доменное исключение с ясным текстом («Метод
Xудалён в ядре 3.0, см. UPGRADE-3.0.md»), а неBadMethodCallException. Публичный рендер темы обёрнут барьером → страница отдаёт последний валидный page-cache или деградированный layout, не 500. - Профилактика:
composer why-not, upgrade-тесты в CI (условие метрики «upgrade-тесты зелёные»),cms:doctor(см. ниже).
Вывод ошибок — три адресата
Одна ошибка — три представления, потому что у неё три читателя.
| Адресат | Канал | Что видит |
|---|---|---|
| Посетитель | публичная страница | Ничего аномального: деградированный блок = нейтральная заглушка или его отсутствие. Никаких stack trace (APP_DEBUG=false инвариант). |
| Контент-менеджер | Filament: бейджи, тосты | «Блок Каталог-грид недоступен (модуль выключен)»; «12 блоков требуют миграции». Действие-подсказка, не техника. |
| Разработчик студии | страница «Обновления», лог, Sentry, телеметрия | Точная причина: версия, класс, разорванная цепочка _v, команда починки. |
Форматы деградации:
- fallback-блок —
blocks/_fallback.blade.php, принимаетreason(missing_module/broken_migration/unknown_type), в debug-режиме показывает детали, в проде — нейтрально. - health-check (
spatie/laravel-health) получает чек «module-integrity»: всеenabledмодули активировались, все используемые блоки резолвятся, цепочки_vзамкнуты. Красный чек → страница/health= 503 → канареечный гейт волны обновлений останавливается. cms:doctor— команда-диагност: сверяет установленные версии пакетов ↔cms_module_versions↔minimum_core_version↔ используемые_vблоков, печатает список рассинхронов с командами починки. Запускается в концеcms:upgradeи по требованию.
Самодиагностика модуля
Модуль — не чёрный ящик: он сам декларирует, как проверить его здоровье (поле health в манифесте):
- свои health-чеки — классы, регистрируемые в общий
spatie/laravel-health: «мои таблицы существуют и мигрированы доschema_version», «внешний API отвечает», «очередь моих джобов не растёт». Попадают в/health, вcms:doctor, в телеметрию — модуль виден флит-дашборду наравне с ядром; - self-test при включении — lifecycle-хук
enableзавершается smoke-проверкой (таблицы, роуты, зависимости резолвятся). Провал → модуль остаётсяinstalled(не enabled) с внятной причиной в UI — вместо «включили и сайт лёг»; - проверка после обновления —
cms:upgradeпрогоняет health-чеки затронутых модулей (шаг 14 процедуры); красный чек модуля = провал волны, откат.
Так «система не ложится намертво» обеспечивается дважды: preflight ловит несовместимость до активации, self-test и health-чеки — деградацию после, и оба говорят словами («Каталогу нужна таблица X, миграция не применена: php artisan migrate»), а не 500.
Изоляция сбоев модуля (заготовка под маркетплейс)
Из безопасности: баг в модуле не должен ронять ядро. Механизм:
- регистрация вклада модуля (блоки, роуты, слушатели) обёрнута try/catch на этапе boot: исключение в
boot()одного модуля переводит этот модуль вdisabledс причиной, а не роняет весь bootstrap; - рендер-путь блока/виджета модуля обёрнут барьером (см. выше);
- слушатель события модуля, бросивший исключение, логируется и не прерывает цепочку остальных слушателей (изоляция в
FilterBus/event-dispatcher ядра).
Это то, что делает «плагин ≠ ядро» не только про права, но и про устойчивость.
Чеклист фазы 0 (жизненный цикл модулей)
- [ ] Машина состояний модуля (installed/enabled/disabled/uninstalled) + переходы через ядро.
- [ ] Три грани версии (пакет/схема БД/
_vблока) + правило semver-соответствия. - [ ] Bootstrap-preflight: dry-run загрузки провайдера до активации.
- [ ] Barrier-рендер блоков/виджетов: fallback вместо исключения (4 сценария A–D).
- [ ] Обратная проверка использования блока при disable/uninstall (
cms_block_usage). - [ ]
cms:module:rollbackсинхронно по трём слоям + честное сообщение о потере данных. - [ ] Три представления ошибки (посетитель/контент-менеджер/разработчик).
- [ ] health-check «module-integrity» +
cms:doctor. - [ ] Самодиагностика:
health-чеки модуля в манифесте + self-test при enable. - [ ] Изоляция сбоя boot/render/listener одного модуля от ядра.
Связи
- Зависимости модулей — граф requires/provides/load_after.
- Центр обновлений — процедура апгрейда и три слоя отката.
- Контракт блока —
_v, data-миграция, fallback-рендер. - Модель данных —
cms_module_versions/history,cms_block_usage. - Безопасность — изоляция сбоев и прав.