Skip to content

Расширяемость и ядро

Главный принцип: 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Декорация сервиса (обёртка с доступом к оригиналу)средняя
4Override шаблона темы (каскад 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.phpcontent--article.blade.phpcontent.blade.php).

Токены темы — theme.json (цвета, шрифты, радиусы, отступы) → генерация CSS-переменных и Tailwind-пресета. Стандарт стилей — Tailwind: смазка для приёма макетов из claude.ai/design.

Контракт модуля

Модуль = composer-пакет, обязан:

  1. Регистрировать типы блоков в BlockRegistry: renderer (blade) + Filament-схема формы
    • writer/seeder + версия схемы.
  2. Привозить свои миграции (таблицы с префиксом модуля, down() обязателен).
  3. Регистрировать роуты через хук ядра, не напрямую Route:: (совместимость с route:cache — урок инцидента TrailingSlashUrlGenerator в universal).
  4. Поставлять Filament-plugin (ресурсы/страницы в общую панель) и свои permissions.
  5. Реагировать на события ядра (PageSaved, LeadCreated, CacheFlushRequested…) — не патчить чужой код.
  6. Декларировать совместимость: "require": {"cms/core-contracts": "^2.0"} (зависимость от контрактов, не реализации) + minimum-core-version в манифесте (проверяет preflight).
  7. Проходить контрактные тесты из пакета 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.

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