Skip to content

Фаза 0 — Жизненный цикл и надёжность модулей

Статус: спека фазы 0. Закрывает рантайм-надёжность, которую граф зависимостей и центр обновлений подразумевали, но не описывали: установка/включение/откат модуля, поведение при рассогласовании версий (старый блок ↔ новый модуль, модуль выключен), и — главное — как это видит человек (понятные ошибки, а не 500). Опирается на зависимости модулей, центр обновлений, контракт блока, модель данных.

Главный инвариант: деградация, а не падение

Правило, которого не было и которое определяет весь дизайн: рассогласование версий никогда не роняет публичную страницу. Отсутствующий модуль, устаревший блок, несовместимая версия — всё это переводит систему в явно деградированное, видимое администратору состояние, но публичный сайт остаётся живым. Урок Битрикс/WP: «белый экран смерти» при конфликте модуля недопустим в managed-парке, где падение видит клиент, а чинит студия постфактум.

Три следствия этого инварианта проходят через всю спеку:

  1. рендер-путь блока/виджета от модуля обёрнут в безопасный барьер (fallback вместо исключения);
  2. любое несоответствие версий обнаруживается заранее (preflight), а не в проде;
  3. каждое деградированное состояние имеет UI-представление в админке и в телеметрии.

Версионирование модуля

Модуль версионируется по semver, и версия имеет три независимых грани — их смешение и есть корень «непонятных ошибок при обновлении».

ГраньЧто версионируетГде живётКто ломается при рассинхроне
Версия пакетакод модуляcomposer.json, composer.lockcomposer (ловится до установки)
Версия схемы БДтаблицы модулямиграции + 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/, но ничего не регистрирует. Данные модуля в БД сохраняются. Это ключевое состояние для сценария «модуль выключен» (см. ниже).
  • uninstalleddown()-миграции, чистка. Данные — по флагу: 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-grid 3→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_versionsminimum_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 одного модуля от ядра.

Связи

Внутренняя база знаний студии