Тема
Стандарт модуля — универсальные требования
Статус: нормативный документ. Все ТЗ модулей пишутся по этому стандарту, код проверяется его гейтами. База: ТЗ ядра + API · обмен данными · контракт модуля · жизненный цикл · спеки студии specs/laravel_13 (обязательны на уровне кода). Формулировки: «обязан» = гейт (нарушение не проходит ревью/CI), «рекомендуется» = дефолт, отступление — с мотивировкой в
docs/module.md.
0. Три инварианта (нарушение любого = модуль не принимается)
- Модуль не может уронить сайт — ни установкой, ни выключением, ни сбоем в рантайме: деградация вместо падения, fallback вместо 500, внятная ошибка вместо белого экрана.
- Модуль не трогает чужого — ни таблиц, ни настроек, ни кода ядра и соседей; весь обмен — через пять каналов (обмен данными).
- Модуль полностью декларативен для платформы — всё, что о нём нужно знать (зависимости, контракты, права, настройки, health, расписание), читается из манифеста и реестров, а не из его исходников.
1. Пакет, структура, именование
- Имя пакета
cms/<slug>, PSR-4Cms\<StudlySlug>\; один модуль = один пакет = одна зона ответственности. - Обязательная структура:
src/(Providers, Services, Models, Http, Jobs, Events, Listeners, Filament, Blocks, Widgets, Health) ·config/<slug>.php·database/{migrations,factories,seeders}·resources/views·routes/{api,web}.php·tests/·docs/module.md·CLAUDE.md. - Модуль регистрируется только через хуки ядра (ServiceProvider + реестры) — никаких правок ядра, чужих модулей, глобальных конфигов.
Канон именования (единый по всей CMS, проверяется ревью):
| Сущность | Формат | Пример |
|---|---|---|
| Таблицы | cms_<slug>_* / коммерция commerce_* | cms_reviews_items |
| Настройки | <slug>.<key> (snake) | reviews.moderation_enabled |
| События | PascalCase, факт в прошедшем времени | ReviewApproved |
| Permissions | <slug>.<action> | reviews.moderate |
| Команды | cms:<slug>:<action> | cms:reviews:recount |
| Очереди | <slug> (+-low/-high при надобности) | import, import-low |
| Теги кеша | <slug>:<entity>[:<id>] | reviews:product:42 |
| API | /api/v1/<slug>/…, /api/v1/admin/<slug>/… | /api/v1/reviews/… |
Исключение канона permissions. Динамические трёхсегментные права вида content.<type-slug>.<view|manage> (средний сегмент — slug типа контента, создаваемого no-code в рантайме) генерирует только ядро — движок типов контента — при сохранении типа, не модуль. Модули по-прежнему обязаны декларировать свои права статически в манифесте (extra.cms.permissions, §2); динамическая генерация прав модулю не разрешена ни в каком виде — см. engine §11.
2. Манифест extra.cms (обязателен)
json
{
"requires": {"cms/commerce-catalog": "^1.0"},
"suggests": {"cms/search": "фасеты быстрее из индекса"},
"conflicts": {},
"provides": ["payment-gateway"],
"load_after": ["cms/commerce-catalog"],
"minimum_core_version": "1.2",
"health": ["Cms\\PayGatewaysRu\\Health\\GatewayCheck"],
"permissions": ["pay-gateways-ru.view", "pay-gateways-ru.manage"],
"settings_groups": ["pay-gateways-ru"],
"lifecycle": {"enable": "…\\Lifecycle\\OnEnable", "disable": "…\\Lifecycle\\OnDisable"}
}requires — только модули; зависимость от ядра — через composer-требование cms/core-contracts (никогда cms/core). provides — имена из канонического реестра контрактов (ниже); новый контракт вводится ревизией ядра, не изобретается модулем.
Канонический реестр provides-контрактов: payment-gateway · delivery-provider · notification-channel · search-provider · captcha-provider · spam-filter · ofd-provider · moderation-workflow · storage-backend · realtime-transport · cdn-provider · mail-transport / sms-transport · crm-connector · erp-connector · rates-provider · suggest-provider · auth-provider · geo-provider · import-target · city-context · marketplace-connector · attack-detection · fraud-signal (ревизия ядра 14.07.2026) · upload-scanner (антивирус-скан загрузок медиатеки) · cabinet-shell · domain-management · fleet-monitoring · marketplace-catalog · two-factor-auth · ugc-publication · update-client (последние семь фактически объявлены ТЗ модулей и легализованы ревизией №2, найдено scripts/check-deps.mjs). Интерфейсы живут в cms/core-contracts; потребитель резолвит реализацию через DI, при нескольких — выбор в настройках, при нуле — деградация с понятным сообщением.
3. Жизненный цикл и версионирование
- Состояния
installed → enabled → disabled → uninstalled; enable запускает self-test (health-чеки модуля) — при провале модуль остаётсяinstalledс внятной ошибкой. - Выключение = деградация: блоки/виджеты отдают fallback, данные не удаляются, публичный сайт жив. Удаление данных — только явный uninstall с подтверждением.
- Выключение модуля-зависимости: preflight disable показывает список зависимых модулей (
requiresна выключаемый) и требует подтверждения. Выключенная зависимость для зависимого модуля = сбой обязательной зависимости: затронутые функции деградируют с внятным сообщением («модерация временно недоступна»), не 500; одобренные/существующие данные продолжают отдаваться. - Обязательные модули managed-парка (
cms/updates,cms/health,cms/sentry,cms/backup,cms/attack-monitor): их disable недоступен клиентским ролям — только studio-роли с подтверждением и записью в аудит; кнопка в админке клиента показывает «модуль обязателен по условиям поддержки». Правило деградации при этом сохраняется: если модуль всё же выключен (студией) или упал, сайт живёт. - Миграции аддитивны к чужим данным; свои таблицы модуль меняет свободно,
down()обязателен (specs: миграции).
Semver модуля. Breaking (только мажор): удаление/переименование события или поля его payload, изменение схемы блока без _v-миграции, изменение сигнатур публичных сервисов и provides-контрактов, удаление API-поля/эндпоинта, деструктивная миграция. Всё это сначала помечается deprecated (минимум один минор) — OpenAPI-метка, @deprecated в коде, запись в docs/module.md. Данные при апгрейде: data-миграции версий блоков (_v, лениво при рендере + батч-командой), батч-команды идемпотентны (повторный прогон безопасен).
4. Данные и БД
- Каждая таблица принадлежит ровно одному модулю (канон именования — §1). Чужие таблицы — только чтение через сервисы владельца.
- По specs: модели и связи:
$castsна каждый JSON/date/bool/enum столбец; обе стороны связей;$withв модели иModel::all()запрещены; eager loading явным->with(). - FK:
constrained()+index(); enum-столбец + PHP Enum; индексы под реальные WHERE/ORDER BY; JSONB → GIN, журналы → BRIN поcreated_at. - Деньги — integer minor units +
currency_code, float запрещён. Финансовые/журнальные таблицы — append-only, исправление — сторно. - Контентные сущности несут измерения
locale,city_id,site_id(nullable) — мультиязычность; скоупы измерений берутся изRequestContext. - Правило измерений: модуль обязан корректно работать, когда измерение отсутствует (сайт без городов/локалей/мультисайта = nullable-измерение, не отдельная ветка кода) — контрактный тест гоняется в обоих режимах.
- Публичные идентификаторы — slug/UUID/ULID; автоинкрементный
idв публичных URL и API-ответах запрещён (перебор ресурсов). - Конкурентное редактирование: сущности, редактируемые в админке, несут
lock_version(optimistic lock) — конфликт отдаёт 409 и человеческое сообщение, а не молчаливый «последний победил». Требование распространяется и наcms_content_entries(записи типов контента движка) — единыйlock_versionдля всех типов, не настройка на уровне конкретного типа (решено 15.07.2026, engine §16). - ПДн-паспорт (матрица v2.2): ТЗ перечисляет столбцы с ПДн, срок хранения и джобу ретеншна; модуль реализует хуки ядра «выгрузить всё по субъекту» и «забыть по запросу» для своих таблиц (152-ФЗ), либо декларирует «ПДн не храню». Технически «хуки ядра» = контракт
PrivacyRegistryизcms/core-contracts(ревизия 14.07.2026, п. 15): модуль регистрирует обработчики, агрегация — командами ядраcms:privacy:export/forget; формулировка «хуки §4» в ТЗ модулей означает именно его.
5. Обмен с ядром и модулями
Строго пять каналов (обмен данными): события · FilterBus · provides-контракты · сервис-вызов по requires · очереди. Запрещены: SQL в чужие таблицы, запись в чужие настройки, вызов чужих классов без requires, HTTP к самому себе, обмен через глобальное состояние.
Минимальный канонический bootstrap (всё расширение — из провайдера, через контракты):
php
final class ReviewsServiceProvider extends CmsModuleProvider
{
public function register(): void
{
$this->app->singleton(ReviewsService::class);
}
public function boot(BlockRegistry $blocks, CacheTags $cache,
ScheduleRegistrar $cron): void
{
$blocks->register(ReviewsBlock::class); // компонент
$cache->declare('reviews'); // теги кеша
$cron->registerScheduleVia(RecountRatings::class);// расписание
Event::listen(OrderCompleted::class, AskForReview::class); // канал 1
}
}6. Настройки
- Все «ручки» — в settings-store ядра, группа =
<slug>; каждая настройка декларируется схемой (тип, дефолт, валидация,affectsPageCache). - Десятки настроек — норма (гибкость), чтение — из кеша группы (0 запросов на горячем пути — контрактный тест).
- Секреты в настройках запрещены — только
.env→config/<slug>.php(anti-hardcode: пороги → config, статусы → Enum, UI-строки →lang/через__()). - Лимиты и квоты — явные настройки с дефолтами (макс. размер, количество, частота); достижение лимита — понятная ошибка и метрика, не 500 и не тихое обрезание.
- Kill-switch: у рискованной функциональности (внешние API, тяжёлые пересчёты, автодействия) — отдельная настройка аварийного отключения без выключения модуля; ядро может дистанционно рекомендовать его через флаги парка (managed-режим).
7. API
Неймспейс /api/v1/<slug>/… (публичное) и /api/v1/admin/<slug>/… (админ) по конвенциям ядра: конверт data/meta, keyset-пагинация, filter[…]/sort по whitelist, коды 401/403/404/409/422/429, Idempotency-Key на мутациях с внешним эффектом, OpenAPI-атрибуты. Контроллеры — по specs: контроллеры: FormRequest (не валидация в контроллере), ≤10 строк логики, Controller → Service → Model.
8. Компоненты
- Блоки: схема полей через движок полей, версия
_v+ data-миграции, demo-props, fallback при выключении; данные рендера — через сервис модуля, не запросы из шаблона. - Виджеты: области + условия показа; отдельная инвалидация кеша областей.
- Filament (specs: filament): ресурсы в неймспейсе модуля,
afterSave()→ инвалидация тегов кеша, авторизация через permissions модуля, все надписи черезlang/(админка мультиязычна). - Команды:
cms:<slug>:*; диагностика обязана поддерживать--json. - Фронтенд-бюджет: JS/CSS блоков и виджетов модуля — только свои ассеты через пайплайн темы; тяжёлое (карты, видео, слайдеры) — lazy-load по видимости; вставка блока не вызывает CLS (резерв размеров); формы доступны с клавиатуры, поля — с label.
- Демо-контент: сидер demo-данных для каждого блока/виджета (галерея
/_galleryи playground обязаны показывать модуль без ручного ввода).
9. Фоновая работа
По specs: services/jobs: очереди именованные (модуль объявляет свои), джобы идемпотентны (повтор не дублирует эффект), слушатели тонкие (валидируют факт → кладут job), внешние HTTP-вызовы — только из очередей (кроме синхронных подсказок с таймаутом и graceful fallback). Расписание — через ScheduleRegistrar ядра, прямой Schedule:: в boot запрещён. Долгие батчи — Bus::batch чанками (утилизация ядер), с прогрессом, доступным админке.
10. Кеш и производительность
- Модуль объявляет свои теги в
CacheTags(канон — §1) и инвалидирует их своими событиями; ручной flush всего кеша запрещён. - Бюджет запросов (performance) — контрактный тест; N+1 = баг (specs: качество).
- Списки — keyset; большие таблицы — проектировать под партиционирование; массовые операции — батчами с батчевой инвалидацией (highload-требования).
- Страницы с персональными данными не попадают в общий page-cache (сегментация — по правилам ядра, не самодельная).
11. Безопасность
- Весь вход — через границы ядра (5 границ): FormRequest-whitelist, санитизация rich-text двойным барьером, файлы через медиатеку, вебхуки через
cms/webhooks-in. - Фильтрация — только стандартизированными обработчиками ядра: rules движка полей → FormRequest, санитайзер rich-text, ReDoS-валидатор пользовательских regex, whitelist фильтров/сортировок. Собственные ad-hoc проверки в контроллерах — незачёт.
- Permissions — только из манифеста + Policies; проверка «по строке роли» запрещена.
- Матрица ролей в ТЗ: для каждого permission — какие штатные роли его получают по умолчанию (админ / менеджер / редактор / studio); опасные действия (удаление данных, массовые операции, доверенный HTML) — только повышенные роли.
- Пользовательские regex — только через ReDoS-валидатор ядра;
{!! !!}— только доверенный HTML под studio-ролью; события безопасности — вcms/auditиcms/attack-monitor(если включены). - В логи и аналитику не попадают ПДн и секреты; идентификаторы — обезличенные.
12. Ошибки и наблюдаемость
- Три аудитории ошибок (lifecycle): посетитель — fallback без деталей; админ — «что случилось и что нажать» на человеческом языке; разработчик/агент — доменное исключение с кодом и текстом «что делать».
catch (\Exception) { return null; }запрещён. - Health-чеки (из манифеста) покрывают минимум: доступность своих зависимостей (внешний API, провайдер), консистентность критичных данных, отставание своих очередей.
- Логи — в канал модуля; метрики горячих операций видны в Pulse; сбои внешних вызовов алертятся через
cms/health, а не молчат.
13. Качество кода и тесты
- specs: качество:
declare(strict_types=1), полная типизация, SRP (300+ строк → разбить), DI через конструктор, доменные исключения, без магии. - Комментарии — щедро, по спеке: docblock «зачем» у каждого класса, PHPDoc публичных методов сервисов, пояснение неочевидных решений по-русски.
- Все UI-тексты модуля (кнопки, подписи, подсказки, ошибки, валидация) — в
lang/пакета, вывод через__('cms-<slug>::…'); строки в коде — незачёт (anti-hardcode). - specs: тесты: Pest 4 + testbench (пакет тестируется в изоляции), контрактный набор
cms-testingкрасный из коробки; feature-тест на каждый роут, factory на каждую модель. ⛔ Политика студии:migrate:fresh/refresh/reset,db:wipeзапрещены — тестовая БД только<slug>_test, пересоздание черезmigrate+ явную проверку имени БД. - Гейт CI:
composer test= Pest + cms-testing + larastan + pint — зелёный.
14. Документация
CLAUDE.md— правила для агента (контракты, точки расширения, что не трогать);docs/module.md— работа модуля и взаимосвязи (схема данных, события, настройки, обратные зависимости, поведение при выключении) — обновляется тем же PR, что и код;- OpenAPI-атрибуты на всех эндпоинтах (автоспека в CI).
15. Эксплуатация (runbook)
- Модуль декларирует свои метрики (счётчики горячих операций, длительности внешних вызовов) и алерты (что считается инцидентом) — видимы в Pulse/health.
- В
docs/module.md— мини-ранбук: «симптом → что проверить → какая команда чинит» для 3–5 типовых инцидентов модуля (внешний API лёг, очередь отстала, данные разошлись). - Восстановительные команды (
cms:<slug>:recount,reindex,repair) — идемпотентны, с--dry-runи--json; допустимы к запуску на живом сайте без даунтайма. - Бэкап/рестор: ТЗ указывает, что из данных модуля попадает в бэкап (обычно все свои таблицы + файлы), а что после рестора пересоздаётся командой (поисковые индексы, денормализованные агрегаты, кеши) — рестор без этой команды не считается завершённым.
- Платные внешние API: расход/остаток квоты виден в админке модуля; исчерпание квоты — деградация с алертом, не 500.
16. Миграция legacy-данных
Для модулей с донорами и для переезда клиентов со старых платформ:
- Команда
cms:<slug>:import-legacy --source=<профиль>— маппинг старых таблиц/экспорта на новую схему; идемпотентна (повторный прогон обновляет, не дублирует — ключexternal_id), поддерживает--dry-runс отчётом расхождений. - Прогон на копии данных — часть приёмки модуля, если у модуля есть донор с боевыми данными (фаза 4 — миграция universal — не должна требовать дописывания импортёров).
- Отчёт импорта: сколько прочитано/создано/обновлено/пропущено и почему — построчные ошибки скачиваемы, молчаливый пропуск запрещён.
Анти-паттерны (то, из-за чего модули заворачивают чаще всего)
- логика в контроллере/слушателе/Blade вместо сервиса; «бог-сервис» на 800 строк;
- свой мини-фреймворк там, где есть механизм ядра (свои настройки, свой кеш, свой рендер);
- скрытая зависимость: вызов чужого класса без
requires— работает, пока сосед включён; - «оптимизация» обходом конвейера (raw SQL в чужую таблицу «для скорости»);
- событие-команда (
SendEmailEvent) вместо факта (OrderPaid) — событие не приказывает, оно сообщает; - инвалидация кеша «на всякий случай» целиком; настройка без дефолта; миграция без
down(); - синхронный HTTP к внешнему API на горячем пути страницы (только очередь либо таймаут ≤2 с + graceful fallback);
- автоинкрементный
idв публичном URL/API; «последний победил» при параллельном редактировании вместо optimistic lock; - ретеншн «потом»: журнальная таблица без политики очистки — это бомба на диске клиента.
Шаблон ТЗ (v2.1, обязательный)
md
# ТЗ — <Название> (`cms/<slug>`)
> Слой · Зрелость доноров · Донор
> Статус: ТЗ к разработке
## Назначение и возможности ← что делает + список возможностей
## Зависимости и выключение ← requires/suggests/provides/conflicts + деградация
## Модель данных ← таблицы, ключевые поля, индексы/casts-заметки
## Входные и выходные данные ← все входы (формы, API, события, импорт, вебхуки)
## и выходы (ответы, события, экспорт, рендер) + форматы
## Настройки (группа `<slug>`) ← ключ, тип, дефолт, affectsPageCache
## API ← /api/v1/<slug> + /api/v1/admin/<slug>, доступ
## Компоненты ← блоки, виджеты, Filament, команды (--json)
## События и обмен ← издаёт / слушает / фильтры / provides-контракты
## + таблица взаимодействий с сущностями других модулей
## Фоновая работа ← джобы, очередь, расписание (или «нет»)
## Производительность и кеш ← ожидаемые объёмы, горячие пути, бюджет запросов,
## теги кеша и инвалидация
## Безопасность ← границы входа, санитизация, rate-limit, права
## UX-требования ← админ (пустые состояния, массовые действия,
## человеческие ошибки) и посетитель
## Крайние случаи и типовые баги ← гонки, дубли, рассинхрон, противоречия настроек
## Донорский код ← только проверенные пути
## Тесты и приёмка ← контрактные тесты + чеклист DoDТребования v2.1 к новым разделам (добавлены 2026-07-14; ТЗ без них считаются не углублёнными и дорабатываются до старта разработки модуля):
- Входные и выходные данные. Полный перечень: откуда вход (форма, API-запрос, слушаемое событие, импорт-файл, вебхук), какие поля, чем валидируется; куда выход (ответ API, издаваемое событие, экспорт, рендер блока/виджета) и в каком формате. Всё, что не перечислено как вход, модуль обязан отвергать (whitelist-принцип §11).
- Взаимодействия (внутри «События и обмен»): таблица «сущность/модуль → канал (1 из 5) → направление → что происходит». Любая связь вне таблицы — скрытая зависимость, анти-паттерн.
- UX-требования. Для админа: пустые состояния с подсказкой «что нажать», массовые действия на списках, ошибки на человеческом языке (аудитория «админ» из §12), подтверждение необратимых операций. Для посетителя: поведение форм при ошибке (сохранение введённого), скорость воспринимаемого отклика, доступность.
- Крайние случаи и типовые баги. 6–12 конкретных сценариев в формате «сценарий → ожидаемое поведение»: гонки (двойной сабмит, параллельный пересчёт), дубли, рассинхрон денормализованных данных, противоречивые комбинации настроек, поведение при выключенном suggests-модуле, сбой внешнего API, пустые/огромные данные, ловушки измерений locale/city/site. Обнаруженное противоречие с ядром или стандартом помечается «⚠️ Противоречие» прямо в этом разделе; неразрешимое — дублируется в открытые вопросы.
Матрица продуманности ТЗ (v2.2)
Дополнение к шаблону v2.1 (2026-07-14): перед стартом разработки ревью ТЗ проверяет, что каждый пункт матрицы либо раскрыт в соответствующем разделе, либо явно помечен «не применимо». Отдельные новые разделы не вводятся — пункты живут внутри существующих:
| Критерий | Что должно быть в ТЗ | Раздел |
|---|---|---|
| ПДн-паспорт | Какие ПДн хранит, срок хранения (ретеншн), участие модуля в «выгрузить всё по субъекту» и «забыть по запросу» (152-ФЗ) | Модель данных / Безопасность |
| Матрица ролей | Роль × действие (админ, менеджер, редактор, studio), не только список permissions | Безопасность |
| Эксплуатация | Метрики и алерты модуля, «что смотреть при инциденте», восстановительные команды (cms:<slug>:*), что попадает в бэкап и что пересоздаётся после рестора | Фоновая работа / Компоненты |
| Миграция legacy | Команда/джоба импорта данных с донорских сайтов (маппинг старых таблиц → новые), идемпотентность повторного прогона | Донорский код |
| Конкурентное редактирование | Два админа правят одну сущность → optimistic locking с внятным конфликтом, не «последний победил» молча | Крайние случаи |
| Лимиты и квоты | Максимумы (размер, количество, частота) — явные настройки с дефолтами; поведение при достижении лимита | Настройки / Производительность |
| Фронтенд-бюджет и a11y | Вес JS/CSS блоков модуля, lazy-load тяжёлого, отсутствие CLS, клавиатурная доступность форм | Компоненты / UX-требования |
| Стоимость внешних API | Тарифные лимиты провайдера, видимость расхода в админке, поведение при исчерпании квоты (деградация, не 500) | Зависимости / Крайние случаи |
| Демо-контент | Сидеры demo-данных для галереи блоков и playground | Компоненты |
| Kill-switch | Аварийное отключение рискованной фичи настройкой без выключения модуля | Настройки |
Чеклист приёмки модуля (DoD)
- [ ] Три инварианта §0 соблюдены (есть контрактные тесты на деградацию и изоляцию);
- [ ] манифест полный,
cms:doctor --jsonзелёный, health-чек и self-test работают; - [ ]
composer testзелёный (Pest + cms-testing + larastan + pint), бюджет запросов пройден; - [ ] все входы валидируются, permissions навешаны, секретов в БД/коде/логах нет;
- [ ] кеш-теги объявлены и инвалидируются событиями; N+1 отсутствует; настройки — 0 запросов;
- [ ] измерения locale/city/site работают и при их отсутствии (оба режима в тестах);
- [ ] semver-политика §3 соблюдена; депрекации задокументированы;
- [ ] OpenAPI покрывает API;
CLAUDE.md+docs/module.mdактуальны; - [ ] anti-hardcode: пороги в config, статусы в Enum, строки UI в
lang/; - [ ] крайние случаи из ТЗ (раздел v2.1) покрыты тестами либо явно помечены «не покрыто — причина» в
docs/module.md; - [ ] матрица продуманности v2.2 закрыта: ПДн-паспорт, матрица ролей, ранбук §15, ретеншн журналов, лимиты с дефолтами, демо-сидеры, kill-switch (или «не применимо»);
- [ ] legacy-импортёр §16 есть и прогнан на копии донорских данных (для модулей с донором);
- [ ] ни один анти-паттерн из списка выше не присутствует.