Тема
Фаза 0 — WidgetRegistry и области
Статус: спека фазы 0. Виджет — отдельная сущность (Q4), не частный случай блока. Здесь — реестр виджетов, области, условия показа и — критично — отдельная инвалидация кеша областей. Параллель BlockRegistry, но с другим жизненным циклом. Опирается на глоссарий → виджет, модель данных, движок полей.
Виджет ≠ блок: почему отдельная сущность
Решение Q4 зафиксировало разные жизненные циклы и места хранения:
| Блок | Виджет | |
|---|---|---|
| Где живёт | в потоке контента страницы | в области (сайдбар/футер/шапка) |
| Хранение | props в JSONB ревизии страницы | своя таблица cms_widgets, привязка к области |
| Охват | одна конкретная страница | сквозной — на многих страницах разом |
| Кто редактирует | контент-менеджер в drag&drop страницы | контент-менеджер в разделе «Области» |
| Инвалидация кеша | постраничная (меняется страница) | по области (меняется на всех страницах) |
Слияние в одну сущность заставило бы тащить «область» в каждый блок и ломало бы инвалидацию: правка виджета в футере затрагивает все страницы, а не одну — это принципиально другая модель сброса кеша.
Определение типа виджета
Как BlockType — объект-описание в открытом реестре. Использует тот же движок полей:
php
WidgetType::make('contacts')
->label('Контакты')
->areas(['footer', 'sidebar']) // где допустим
->fields(fn () => [ /* Field-декларации движка полей */ ])
->view('widgets.contacts') // резолвится каскадом тем
->schemaVersion(1)
->interactive(false) // React-остров, как у блока
->cacheScope('area'); // область как единица кеша (см. ниже)Регистрация — в boot() провайдера модуля/темы/проекта (открытость, как у BlockRegistry):
php
WidgetRegistry::register(ContactsWidget::describe());Области (areas)
Область — именованное место в макете, куда тема выводит виджеты. Базовый набор ядра — header, sidebar, footer; тема объявляет свои через контракт темы.
blade
{{-- в layout темы --}}
<x-cms-area name="footer" /> {{-- рендерит все виджеты области footer по условиям показа --}}Тема декларирует доступные области в манифесте (контракт темы); ядро валидирует, что виджет размещают в области, которую тема поддерживает.
Хранение и условия показа
cms_widgets
id, type, # тип из WidgetRegistry
area, # footer / sidebar / header / …
props (jsonb), # значения полей (движок полей)
sort_order,
conditions (jsonb), # ГДЕ показывать (см. ниже)
is_active, _v,
locale / city_id (nullable), # заготовка мультигорода/локали (Q6)
created_at, updated_atУсловия показа (conditions) — где виджет виден, аналог видимости виджетов WordPress, но декларативно:
jsonc
{
"include": { "page_type": ["catalog", "service"], "url": ["/about*"] },
"exclude": { "url": ["/checkout*"] }
}Резолвер условий на рендере области решает, какие виджеты показать на текущей странице. Условия — данные, не код (не даём контент-менеджеру писать логику).
Инвалидация кеша областей — ключевое отличие
Виджет сквозной → его правка меняет все страницы, где видна область. Поэтому кеш областей инвалидируется отдельно от постраничного:
- каждая область — свой тег кеша (
area:footer,area:sidebar); - правка виджета (Filament
afterSave) →Cache::tags(['area:footer'])->flush(), не сброс конкретной страницы и неCache::flush()всего (filament spec); - страница в page-cache хранит области как отдельные кешируемые фрагменты (edge-side-includes- подобно) или помечена тегами всех областей, что на ней есть — сброс тега области инвалидирует затронутые страницы;
- событие
WidgetSaved(ось событий) → слушатели (CDN-инвалидация, прогрев) знают, что изменилась область, а не страница.
Это то, что было бы невозможно чисто, будь виджет частным случаем блока.
Рендер
php
// рендер области
$widgets = WidgetRepository::forArea('footer', context: $pageContext) // с учётом conditions
->visible(); // is_active + условия
foreach ($widgets as $w) {
$type = WidgetRegistry::get($w->type);
$props = WidgetMigrator::upgrade($type, $w); // до актуальной _v (как у блока)
echo view($type->resolveView(), ['props' => $props, 'widget' => $w]);
}resolveView()— каскад тем (контракт темы):проект → тема → parent → fallback ядра;- неизвестный тип (модуль выключен) → fallback-заглушка, не падение (деградация);
- вывод — только
; интерактивный виджет — server-HTML + React-остров.
Версионирование
_v виджета и data-миграция props — та же механика, что у блока (контракт блока). Тип виджета объявляет миграции; применение — лениво при рендере или батчем cms:widgets:migrate.
Стыки с жизненным циклом модуля
- виджет модуля исчезает из областей, когда модуль выключен (сценарий B из жизненного цикла): область просто не рендерит виджет, страница цела;
- при disable/uninstall модуля — обратная проверка: «виджет модуля используется в N областях» (аналог
cms_block_usage); - перенос сайта: виджеты входят в дамп + переносимый демо-контент темы (модель данных).
Контрактные тесты виджета (cms-testing)
demo-props валидны против схемы (движок полей);- рендер
demoне бросает исключений, нет{!! !!}вне whitelist; - цепочка
_vзамкнута (1→…→current); - виджет размещается только в объявленных
areas; - правка виджета инвалидирует тег области, не всю страницу и не весь кеш;
- условия показа (
conditions) резолвятся корректно (include/exclude).
Чеклист фазы 0 (WidgetRegistry)
- [ ]
WidgetType+ открытыйWidgetRegistry(регистрация модулем/темой/проектом). - [ ] Области:
<x-cms-area>, декларация областей темой, валидация размещения. - [ ] Таблица
cms_widgets(props jsonb, conditions jsonb, area,_v, locale/city). - [ ] Условия показа (include/exclude) — данные, не код.
- [ ] Отдельная инвалидация кеша по тегу области (
area:*), не постраничная. - [ ] Версионирование
_v+ data-миграция (как у блока). - [ ] Fallback-рендер при неизвестном типе / выключенном модуле.
- [ ] Контрактные тесты виджета.
Связи
- Решение Q4, глоссарий → виджет.
- Контракт блока — параллельная механика (
_v, миграции, fallback). - Движок полей — схема полей виджета.
- Контракт темы — области, каскад шаблонов.
- Модель данных —
cms_widgets, перенос. - Жизненный цикл модулей — виджет при выключенном модуле.
- filament spec — инвалидация тегами.