besnovatyj / yii2-cms-modman
Система управления модулями Yii2 CMS (compile-not-patch): discovery, реестр состояния, компиляция производных конфигов, lifecycle установки/удаления модулей.
Package info
github.com/besnovatyj/yii2-cms-modman
Type:yii2-extension
pkg:composer/besnovatyj/yii2-cms-modman
Requires
- php: >=8.4
- besnovatyj/yii2-cms-contracts: ^3.0
- besnovatyj/yii2-cms-kernel: ^1.0
- yiisoft/yii2: ~2.0.0
Requires (Dev)
- roave/security-advisories: dev-latest
Suggests
- composer/semver: Полноценная проверка ограничений версий (^, ~, диапазоны) в зависимостях модулей
Provides
None
Conflicts
None
Replaces
None
README
Система управления модулями CMS на принципе compile-not-patch: состояние модулей декларативно и единично (реестр), а вся Yii-конфигурация — производная и компилируется заново. Модуль на стадии тестирования.
Пришла на смену прежнему патч-based modman. Артефакты пишутся в канонические пути конфигурации
приложения — без суффиксов и аддитивных слияний.
- Идея и слои — ARCHITECTURE.md
- Сравнение «патч-modman → modman» — таблица в ARCHITECTURE.md §12
Карта каталога
catalog/ ModuleManifest + value-объекты, discovery (composer installed.json), ManifestFactory,
PackageCatalog, InvalidModule, маркер CmsMarker/CmsKind (extra.bescms)
registry/ ModuleStatus, Version, ModuleState, ModuleRegistry (атомарный lock-файл — источник истины)
compiler/ AtomicWriter, ConfigCompiler (чистая компиляция), MergePlanCompiler, ArtifactPaths,
MenuCompiler (раскладка пунктов меню админки по локациям)
menu/ MenuProvider — меню админки по локациям на запросе (группа admin-menu)
deps/ SemverConstraint, DependencyGraph, DependencyResolver (прямые + обратные зависимости)
migration/ MigrationOwnershipRepository, ModuleMigrationRunner (учёт владения, pending-only update)
lifecycle/ Planner/Executor/Steps/Handlers (mutex, commit-at-end, компенсация, reconcile, sync)
events/ ModuleLifecycleDispatcher (app-level шина межмодульных интеграций)
controllers/ backend/ModulesController (веб-интерфейс)
commands/ ModulesController (консольный драйвер поверх того же фасада)
ModuleManager.php фасад — единый публичный API для драйверов
Контракты модуля вынесены в пакеты. Capability-интерфейсы (
DeclaresModule,Provides*) живут в пакетеbesnovatyj/yii2-cms-contracts(Besnovatyj\Contracts\module\*), а рантайм-базовый класс — вbesnovatyj/yii2-cms-kernel(Besnovatyj\Kernel\module\CmsModule). Менеджер и модули используют версии из пакетов; локального каталогаcontract/больше нет.
Подключение в приложение
1. Глобальный bootstrap — в app/common/config/main.php (app-level bootstrap), чтобы DI-проводка
и шина событий поднимались рано, до загрузки скомпилированных артефактов:
'bootstrap' => ['log', 'queue', \Besnovatyj\Modman\Bootstrap::class],
Bootstrap поднимает DI-контейнер менеджера и его канал лога modman/*. Это же решает chicken-and-egg:
менеджер обязан работать, чтобы скомпилировать конфиги, поэтому его проводка — глобальная, а не из
компилируемого артефакта.
2. Merge-plan — это и есть конфиг приложения. Менеджер пишет @config-dyn-gen/merge-plan.php:
какие extra.config-plugin-файлы активных модулей входят в какую группу движка yiisoft/config
(common, app-*, admin-menu). Приложение собирает по нему свой конфиг, а админка — меню
(см. «Меню админки» ниже). Рядом лежат артефакты, которые движком не покрываются: лог-каналы, реестр
опций, манифест источников представлений, плитки дашборда (params.artifacts).
3. Холодный старт — автоматический. Bootstrap (шаг 1) сам регистрирует модуль Modman в
приложении (Application::setModule), поэтому менеджер доступен даже когда modulesConfigFile.php
пуст, устарел или ссылается на мёртвый старый id/namespace. Это разрывает chicken-and-egg: инструмент,
который компилирует артефакт модулей, не зависит от этого артефакта, чтобы запуститься, — руками
артефакт править не нужно (и нельзя, closed-loop compile-not-patch).
Регистрация идемпотентна (guard hasModule): как системный модуль (editable=false) Modman всё
равно попадёт в modulesConfigFile.php при следующей recompile и переживёт любую пересборку (см.
ARCHITECTURE §6); саморегистрация в bootstrap — постоянная страховка на холодный старт, а не костыль.
Использование
Веб: /Modman/backend/modules/index — список модулей/пакетов (фильтры по статусу/обновлениям,
сортировка, пагинация), «План» (dry-run), установка, обновление, удаление, «Сверка» (reconcile),
«Пересобрать конфиг», «Итоговый конфиг». (sync — пока только в консоли, см. ниже.)
Консоль:
php yii Modman/modules/list
php yii Modman/modules/check <moduleId>
php yii Modman/modules/install <moduleId>
php yii Modman/modules/update <moduleId>
php yii Modman/modules/uninstall <moduleId>
php yii Modman/modules/reconcile
php yii Modman/modules/sync [--adoptAll] # пересобрать реестр из реальности (см. ниже)
php yii Modman/modules/recompile
(Для консоли модуль также должен быть в modules console-приложения.)
Меню админки
Меню не компилируется в файлы: его собирают на каждом запросе админки из группы admin-menu.
- Модуль кладёт пункты в
src/config/adminMenu.phpи объявляет файл вcomposer.json:extra.config-plugin.admin-menu: "src/config/adminMenu.php". Пункт — обычный пунктNavWidgetс_meta.placements: спискомBesnovatyj\Contracts\adminMenu\AdminMenuPlacement(там же описан формат). Локации — только изAdminMenuLocation; правила сортировки — докблокcompiler/MenuCompiler. Пункт без размещений или с размещением другого типа —InvalidConfigExceptionпри раскладке. - При install/update/uninstall/recompile файл попадает в merge-plan только у активного модуля.
- На запросе движок yiisoft/config
require-ит файлы активных модулей (замыканияactiveостаются живыми) и склеивает их в плоский список.MenuProvider::forLocation()раскладывает его по локациям один раз за запрос. - Потребители берут свою локацию по имени и сами фильтруют пункты правами
(
Besnovatyj\Kernel\security\MenuAccessFilter): сайдбары layout'а админки (LeftSidebar,RightSidebar), модуль admin-panel (шапкаHeaderQuickLinks, палитра команд, страница настроек).
Правка содержимого adminMenu.php не требует recompile (на проде, как и любой PHP-код, — после сброса
OPcache). Recompile нужен только при изменении состава модулей или их extra.config-plugin.
sync — восстановление реестра из фактического состояния
Реестр modules-state.php — единственный источник истины, но если его файл затёрли/побили, штатного
способа восстановить его «из реальности» раньше не было (reconcile чинит только транзиентные записи,
уже присутствующие в реестре). sync закрывает эту точку отказа — без ручной правки файла и без
ручных контрольных сумм:
- набор модулей берётся из discovery (
vendor/composer/installed.json); - применённые миграции — из БД-истории владения (
MigrationOwnershipRepository); manifestChecksum— пересчитывается детерминированно фабрикой манифестов.
«Установлен по факту» = уже installed в реестре, или за модулем числятся применённые миграции в
БД, или передан --adoptAll. Иначе модуль пропускается (используйте install). После пересборки
запускается recompile. Осиротевшие записи реестра (пакет исчез из каталога) не удаляются — только
сообщаются. --adoptAll (-a) — крайняя мера: усыновить как installed все обнаруженные
editable-модули для голого восстановления.
Маркер пакета CMS (extra.bescms)
Менеджер показывает только пакеты, явно помеченные как часть этой CMS, — чужие composer-зависимости
из vendor/ (после переезда в GitHub их будут сотни) в список не попадают. Маркер живёт в composer.json
в блоке extra (трогать type: yii2-extension нельзя — по нему Yii подключает свои bootstrap):
"extra": { "bescms": {"kind": "module"} }
kind: "module"— полноценный управляемый модуль. Должен также объявлятьextra.moduleClass/moduleIdи реализовывать контрактDeclaresModule(см. ниже). Попадает во вкладку «Модули».kind: "package"— пакет CMS без жизненного цикла (например, виджет). Виден во вкладке «Пакеты», но не устанавливается менеджером.
Пакет без маркера менеджер игнорирует. Модуль, помеченный kind: module, но с ошибкой конфигурации
(нет moduleClass, не реализует контракт, дубликат id), показывается строкой с причиной и погашенной
кнопкой установки; подробности уходят в лог-канал modman/*, а не во flash на всю страницу.
Контракт модуля
Модуль наследует тонкий рантайм-базовый класс Besnovatyj\Kernel\module\CmsModule (раскладка
controllerNamespace по контексту приложения, layout из активной темы, хук DI /config/container.php) и
реализует DeclaresModule плюс нужные Provides* (статические методы — discovery не инстанцирует класс):
use Besnovatyj\Kernel\module\CmsModule; use Besnovatyj\Contracts\module\DeclaresModule; use Besnovatyj\Contracts\module\ProvidesMigrations; final class Module extends CmsModule implements DeclaresModule, ProvidesMigrations { public static function moduleId(): string { return 'Shortcode'; } public static function isEditable(): bool { return true; } public static function moduleConfig(): array { return ['id' => 'Shortcode', 'params' => [...]]; } public static function migrationPath(): string { return __DIR__ . '/migrations'; } public static function migrationNamespace(): ?string { return __NAMESPACE__ . '\\migrations'; } }
Соответствующий composer.json модуля объявляет и маркер, и класс/id:
"extra": { "bescms": {"kind": "module"}, "moduleClass": "Besnovatyj\\Shortcode\\Module", "moduleId": "Shortcode" }
Версия модуля — только git. В модуле версии нет. Modman берёт её из
installed.json: тег, по которому composer поставил пакет (v1.2.0); для ветки —0.0.0-dev+<sha7>коммита. Новая версия модуля = новый тег +composer update.
Модули — только composer-пакеты. Каталог видит лишь то, что установлено composer'ом; локальных модулей и скана директорий нет. Новый модуль создаётся сразу в
vendor/besnovatyj/<pkg>, пушится и подключается черезcomposer update.
Размещение контрактов. Контракты и базовый класс вынесены в отдельные пакеты (
besnovatyj/yii2-cms-contractsиbesnovatyj/yii2-cms-kernel), поэтому модули не зависят от менеджера.
Сам менеджер — обычный модуль.
modman/Moduleреализует тот жеDeclaresModuleсisEditable() === false: он виден в общем списке как «системный» (с версией, без кнопок установки/удаления), а не как исключение. Его проводка — глобальная (шаг 1), а в компиляцию он попадает как любой системный модуль.
Проверка синтаксиса (Docker)
docker compose exec php sh -c 'find /home/node/app/vendor/besnovatyj/yii2-cms-modman/src -name "*.php" -print0 | xargs -0 -n1 -P4 php -l'
Описание функционала и логики
Кнопка "Обновить" модуль
Это кнопка update-lifecycle модуля в modman — не git, не composer, не upstream-проверка. Она пересобирает интеграцию уже лежащего на диске кода модуля в приложение.
Путь: POST → actionUpdate → ModuleManager::update() → UpdateHandler::update().
Когда появляется: только если installed && hasUpdate, где
hasUpdate = модуль активен И (версия манифеста > версии в реестре ИЛИ checksum манифеста ≠ записанного в реестре).
То есть код на диске изменился (новый тег/новый вклад), а реестр modman ещё отражает старое состояние.
Что делает (под LifecycleLock + мьютексом):
- Пишет в реестр статус
Updating(write-ahead intent), шлёт событиеBeforeUpdate(на него могут реагировать другие модули). - Применяет только pending-миграции модуля — благодаря учёту владения миграциями, без
down→up/переустановки (в старом modman «обновление» = снести и поставить заново; здесь — настоящий инкрементальный апдейт). - Создаёт недостающие директории модуля (
@static/...). - Commit: обновляет запись в реестре — новая
version, новыйchecksum,updatedAt = now, список применённых миграций дополняется, статус →Installed. LifecycleExecutorв конце перекомпилирует конфигурацию (merge-plan/артефакты) из реестра.- При успехе — событие
AfterUpdateи отчёт «обновлён до vX» в модалке; при ошибке — rollback к прежнему установленному состоянию.
Чего она НЕ делает: не тянет новый код с GitHub и не запускает composer update — предполагается, что новый код уже
на диске (composer его уже поставил). Кнопка лишь синхронизирует под него БД-миграции, директории, реестр и
скомпилированный конфиг.
Кнопка-иконка «обновить, минуя кэш» в колонке Upstream — другое: она лишь заново запрашивает последний тег с GitHub, ничего в системе не меняя.