Тема
Расширяемость и ядро
Главный принцип: Laravel уже даёт лучший фундамент ядра CMS. Не копировать чужие ядра, а достроить поверх Laravel недостающие примитивы. Гибкость «перехватов» в стиле Битрикс
/local+init.phpв Laravel делается чище — без свалки.
Почему Laravel — сильнейший фундамент
Ни одна из «больших» CMS (Битрикс, WordPress, Drupal, TYPO3) не имеет того, что у Laravel есть бесплатно: DI-контейнер, типизированные события, service providers, package auto-discovery, обратимые миграции, и — главное — встроенную границу «ядро / пользовательский код» (vendor/ против app/).
Именно эту границу другие городят вручную: Битрикс — приоритетом /local над /bitrix, WordPress — через wp-content/, Drupal — через core/. У Laravel она из коробки.
Стратегия: ядро и модули — приватные composer-пакеты в vendor/; проектная кастомизация — в app/. Обновление ядра (composer update) физически не может тронуть app/.
Аналог Битрикс /local + init.php
В Битрикс /local — папка, которую обновление ядра гарантированно не трогает, а /local/php_interface/init.php автоподключается на каждом запросе (туда вешают перехваты). В Laravel:
/bitrix→ ядро как пакетvendor/cms/core;/local→ папкаapp/Local/в проекте клиента;init.php→ не файл-свалка (это признанный антипаттерн самого Битрикса), а папка авто-подхватываемых классов-обработчиков.
php
// app/Providers/LocalServiceProvider.php — «init.php» по-ларавельному
public function boot(): void
{
// «положил класс-обработчик в папку — он подхватился»
foreach (glob(app_path('Local/Hooks/*.php')) as $file) {
$class = 'App\\Local\\Hooks\\'.basename($file, '.php');
foreach ($class::subscribes() as $event => $method) {
Event::listen($event, [$class, $method]);
}
}
}Гибкость /local/php_interface (кинул файл — работает), но каждый перехват — типизированный класс с внедрением зависимостей, а не строки в глобальной свалке. От Битрикс берём изоляцию по путям, от Drupal 11 — класс вместо процедуры.
Три примитива, которых у Laravel нет — достраиваем
1. Фильтры (из WordPress)
События Laravel не умеют «пропустить значение по цепочке и вернуть изменённое» — а это ядро расширяемости CMS: дай модулю поменять HTML контента, пункты меню, SEO-теги. Реализуется на Illuminate\Pipeline:
php
// ядро пропускает контент через все зарегистрированные фильтры
$html = app(FilterBus::class)->apply('content.render', $html, $context);
// модуль/local регистрирует фильтр с приоритетом (как в WordPress)
FilterBus::add('content.render', fn ($html, $ctx) => $html.'<div>баннер</div>', priority: 20);Внутри FilterBus — сортировка по приоритету (ksort, как WordPress WP_Hook) и прогон через Pipeline. Аналогов в остальных трёх системах нет.
2. Управление порядком слушателей (Drupal 11 / TYPO3)
Laravel Event::listen не даёт декларативно управлять порядком. Заимствуем атрибуты before/after/priority:
php
#[CmsListener(event: ContentRendering::class, priority: 100, after: [SeoListener::class])]
final class MyListener { public function handle(ContentRendering $e): void {} }Провайдер читает атрибуты рефлексией, строит топологический порядок, регистрирует в нужной последовательности. Решает главную боль implicit-хуков WordPress — непредсказуемый порядок.
3. Открытый BlockRegistry
Текущий BlockRegistry в проекте universal — жёсткий const BLOCKS со списком классов + enum BlockType. Работает внутри одного проекта, но не расширяем: сторонний модуль не добавит свой блок, не отредактировав файл в ядре.
Переделать в открытый реестр: модуль регистрирует свой блок в boot() своего провайдера, не трогая ядро. Тип блока — объект-описание, а не значение enum:
php
BlockType::make('hero')
->schemaVersion(3)
->view('blocks.hero') // резолвится через цепочку тем
->fields(fn () => [...]) // Filament-схема формы (ленивая) — API контракта блока
->demo([...]) // props для галереи блоков
->interactive(false); // true = блоку разрешён React-островЯдро, модули и сам клиентский проект регистрируют типы одинаково — кастомный блок клиента не требует форка чего-либо.
Лестница расширяемости — золотая середина
Все четыре «больших» CMS сходятся к одной иерархии способов кастомизации. Официальная формулировка TYPO3: события > hooks > decoration > замена класса. Правило: бери самую верхнюю ступень, что решает задачу — чем ниже, тем хрупче при обновлениях.
| Уровень | Способ | Стабильность при обновлении |
|---|---|---|
| 1 | Событие / фильтр | максимальная |
| 2 | Конфиг / реестр (регистрация блока, канала, типа поля) | высокая |
| 3 | Декорация сервиса (обёртка с доступом к оригиналу) | средняя |
| 4 | Override шаблона темы (каскад theme→parent→core) | средняя |
| 5 | Замена класса целиком (аналог TYPO3 XCLASS) | низкая — «last resort» |
Правило для ТЗ: ядро обязано давать достаточно точек уровней 1–2, чтобы 90% задач решались там. Если модуль вынужден спускаться на уровень 5 — это сигнал, что ядру не хватает точки расширения, и её надо добавить в ядро, а не героически заменять класс. Так эволюционируют зрелые CMS: каждый форк ядра клиентом → кандидат в новую официальную точку расширения.
Каскад тем
Цепочка поиска шаблона через множественные view-namespace с приоритетом:
проект (app/Local) → активная тема → родительская тема → нейтральный fallback ядраПлюс template suggestions в духе Drupal: от специфичного к общему (content--article--teaser.blade.php → content--article.blade.php → content.blade.php).
Токены темы — theme.json (цвета, шрифты, радиусы, отступы) → генерация CSS-переменных и Tailwind-пресета. Стандарт стилей — Tailwind: смазка для приёма макетов из claude.ai/design.
Контракт модуля
Модуль = composer-пакет, обязан:
- Регистрировать типы блоков в BlockRegistry: renderer (blade) + Filament-схема формы
- writer/seeder + версия схемы.
- Привозить свои миграции (таблицы с префиксом модуля,
down()обязателен). - Регистрировать роуты через хук ядра, не напрямую
Route::(совместимость сroute:cache— урок инцидентаTrailingSlashUrlGeneratorв universal). - Поставлять Filament-plugin (ресурсы/страницы в общую панель) и свои permissions.
- Реагировать на события ядра (
PageSaved,LeadCreated,CacheFlushRequested…) — не патчить чужой код. - Декларировать совместимость:
"require": {"cms/core-contracts": "^2.0"}(зависимость от контрактов, не реализации) +minimum-core-versionв манифесте (проверяет preflight). - Проходить контрактные тесты из пакета
cms-testing.
Манифест — подход Statamic (extra.{vendor} в composer.json), расширенный полем minimum-core-version, которого нет ни у Statamic, ни у Twill.
Версии схем блоков — условие безопасного центра обновлений
Скрытый айсберг блочных CMS: props блоков лежат в JSONB у клиента, а схему блока меняет обновление модуля. Старые записи перестают соответствовать новой схеме → рендер падает или тихо теряет контент.
Решение (аналог deprecations в Gutenberg WordPress):
- каждый блок хранит
_v(версию схемы на момент записи); - тип блока объявляет карту data-миграций:
->migrations([1 => MigrateV1ToV2::class, …])(from-version → класс, см. контракт блока); - применение — лениво при рендере (с записью результата) или батчем
php artisan cms:blocks:migrate; - правило BC: минорное обновление меняет схему только аддитивно; ломающее изменение схемы = мажор модуля + обязательная data-миграция.
Без этого механизма центр обновлений опасен — это условие его существования.
Безопасность: плагин ≠ ядро по правам
Главная общая уязвимость Битрикс и WordPress — безопасность как «ответственность разработчика», а не enforced-by-default. Результат: 91% уязвимостей WordPress — в плагинах; в Битрикс — CVE в модулях vote/landing (CVSS 10/10, сотни тысяч сайтов).
В нашей CMS сделать невозможным выключить безопасность случайно:
- CSRF — глобальный middleware (не ручной вызов, как
bitrix_sessid_post()); - вывод — только
escaping;{!! !!}линтер запрещает вне whitelisted SEO-блоков; - права модуля ограничены — не давать плагину неограниченный доступ к БД как WordPress-
$wpdb; Policy-слой и capability-модель, чтобы «плагин ≠ ядро по правам».
Реестр точек расширения
Достроить команду php artisan cms:hooks, показывающую все точки расширения ядра, их payload-типы, зарегистрированных слушателей и порядок. Превращает implicit pub/sub в документированный контракт — чего нет ни у одной из четырёх «больших» систем.
Что НЕ повторять из чужих ядер
- init.php-свалку (Битрикс) — классы в папке вместо строк в глобальном файле.
- WP-style
do_action/apply_filtersглобальными функциями (Botble singleton-реестр) — анти-Laravel магия; правильнее нативные Events + Filament render hooks. - Собственный транспорт обновлений (Winter gateway-zip / Botble overwrite ФС) — только
composer updateиз Satis. - Глобальное состояние и «плагин = ядро по правам» (WordPress).
- Четыре конфиг-синтаксиса (TYPO3: PHP+Fluid+TypoScript+YAML).
- Директорный PluginManager-скан — штатное composer auto-discovery.