0% прочитано

Registry Pattern у Laravel: як додавати Content Blocks без giant switch

У Page Builder ICanUp кількість контентних блоків поступово виросла від простих Paragraph і Image до Hero, Cards, FAQ, Forms, Model Collection та блоків з обмеженою областю використання. У статті я показую, як Registry Pattern дозволив об’єднати описи блоків навколо спільного контракту, знаходити потрібну реалізацію за типом і розширювати систему без одного великого `switch`.

27 вересня 2026 р. 10 хв читанняЛаравель

Registry Pattern у ICanUp з'явився не тому, що я вирішила додати ще один design pattern у код.

Причина була практична: Page Builder постійно ріс. Спочатку система мала кілька простих Content Blocks, але згодом з'явилися Hero, CTA, Highlight, Cards, Steps, FAQ, Model Collection, Forms та спеціалізовані blocks.

Якби кожен новий тип блока вимагав додавати ще один switch у валідацію, налаштування редактора, значення за замовчуванням і доменні сервіси, один центральний клас дуже швидко перетворився б на місце, яке доводиться змінювати щоразу при появі нового блока.

Registry не прибирає всі умовні перевірки із системи. Його завдання інше: дати кожному типу стабільний шлях до відповідної реалізації й не змушувати кожну частину системи знати про всі конкретні класи.

Проблема починається не з switch, а з кількості місць, які треба змінити

Один match або switch сам по собі не є проблемою. Проблема починається тоді, коли знання про всі типи блоків розповсюджується по різних частинах системи.

Кожен новий Content Block потребує типу, назви, початкових даних, правил валідації та локалізованих назв полів. Для блоків, що відповідають за подання, додатково потрібні варіанти оформлення та доступні налаштування. Частина блоків також підтримується не для кожної моделі, до якої можна додавати контент.

Якщо всі ці рішення зібрати в одному центральному switch, то додавання нового блока щоразу вимагатиме редагувати велику конструкцію, яка вже знає про Paragraph, Image, Code, Hero, FAQ, Forms і всі типи, що з’являться пізніше.

Без Registry

З Registry

Частина системи, яка використовує блоки, має знати про всі конкретні класи.

Частина системи запитує потрібний опис блока за його типом.

Додавання нового типу вимагає змінювати центральну умовну конструкцію.

Новий тип додає власний опис і підключається до Registry.

Правила валідації та початкові значення можуть почати відрізнятися між різними шарами системи.

Правила валідації та початкові значення зберігаються в єдиному контракті опису блока.

Перевірка підтримки блока для різних моделей легко дублюється.

Registry централізує перевірку того, де саме підтримується блок.

Спочатку потрібен стабільний контракт для описів блоків

PHP - Єдиний контракт для описів Content Blocks
interface ContentBlockDefinitionInterface{    public function type(): ContentBlockTypeEnum;     public function label(): string;     public function defaultData(): array;     public function rules(): array;     public function attributes(): array;}

Registry має сенс лише тоді, коли всі зареєстровані об’єкти працюють за спільним контрактом. У моєму випадку кожен опис блока відповідає за власний тип і за все, що безпосередньо стосується саме цього типу.

Наприклад, блок Code сам визначає правила для мови, коду, заголовка, нумерації рядків і підсвічених рядків. Блок Notice має зовсім іншу структуру даних. Registry не намагається знати ці деталі.

ContentBlockRegistry знає про доступні реалізації, але не втручається в їхню внутрішню логіку

PHP - Знайти опис блока за його типом
final readonly class ContentBlockRegistry{    /**     * @param iterable<ContentBlockDefinitionInterface> $definitions     */    public function __construct(        private iterable $definitions,    ) {}     public function all(): array    {        return $this->serializeDefinitions(            $this->definitions,        );    } 

Registry не створює блоки й не виконує валідацію запиту самостійно. Він лише зберігає набір доступних реалізацій і відповідає на одне запитання: який опис відповідає цьому типу блока?

Це невелика відповідальність, але вона прибирає знання про відповідність «тип блока - конкретний клас» із коду, який використовує ці блоки.

Service Provider збирає Registry в єдиній точці конфігурації залежностей

PHP - Реєстрація описів блоків у контейнері застосунку
final class ContentBlockServiceProvider    extends ServiceProvider{    private const array DEFINITIONS = [        ButtonBlockDefinition::class,        CodeBlockDefinition::class,        DividerBlockDefinition::class,        GalleryBlockDefinition::class,        HeadingBlockDefinition::class,        ImageBlockDefinition::class,        RichTextBlockDefinition::class,        BeforeAfterBlockDefinition::class,        NoticeBlockDefinition::class,        HeroBlockDefinition::class,        CtaBlockDefinition::class,        HighlightBlockDefinition::class,

Конкретні класи все одно потрібно десь перелічити. Registry Pattern не знаходить їх автоматично.

У ICanUp цю роль виконує Service Provider. Це єдина точка, де збираються залежності модуля: тут контейнер Laravel створює конкретні описи блоків і передає їх у Registry.

Важлива різниця в тому, що список реалізацій зберігається в одному місці інфраструктурного шару, а не дублюється в кожному сервісі чи класі запиту.

Валідація отримує правила з опису блока, знайденого за його типом

PHP - Отримати правила валідації для конкретного типу блока
private function resolvedBlockDefinitions(): array{    $definitions = [];    $blocks = $this->input(        'blocks',        [],    );     foreach (        $blocks        as $index => $block    ) {        $type =            $block['type']            ?? null; 

Рівень обробки запиту не повинен знати, що тип code відповідає CodeBlockDefinition, а faq - FaqBlockDefinition.

Він отримує тип із вхідних даних, просить Registry повернути відповідний опис блока, а потім використовує його rules() і attributes().

Завдяки цьому загальна логіка валідації залишається спільною, а правила, специфічні для конкретного блока, зберігаються поруч із його власним контрактом.

Registry виріс: не кожен блок підходить для кожної моделі

PHP - Фільтр описів блоків за типом моделі, до якої їх можна додавати
public function allFor(    string $blockableType,): array {    return $this->serializeDefinitions(        collect($this->definitions)            ->filter(                fn (                    ContentBlockDefinitionInterface $definition                ): bool =>                    $this->supportsDefinition(                        $definition,                        $blockableType,                    )            ),    );}

Згодом простого пошуку за типом стало недостатньо. Деякі Content Blocks мають сенс лише для певних моделей.

Я не хотіла, щоб Page, Post, News та інші частини системи самостійно підтримували списки дозволених типів блоків. Тому Registry отримав методи allFor() і supports().

Глобальні описи блоків доступні для всіх моделей. А опис блока з обмеженою областю використання сам визначає, чи підтримує конкретний тип моделі, до якої можна додавати контент.

Один Registry може повертати не лише реалізацію блока, а й метадані для редактора

PHP - Передача описів блоків у форматі, потрібному редактору
private function serializeDefinitions(    iterable $definitions,): array {    return collect($definitions)        ->map(            static fn (                ContentBlockDefinitionInterface $definition            ) => [                'type' =>                    $definition->type()->value,                 'label' =>                    $definition->label(),                 'default_data' =>                    $definition->defaultData(),

Registry також став межею між доменними описами блоків і редактором в адмінпанелі.

Редактор може отримати тип блока, локалізовану назву, початкові дані та метадані для відображення без окремої жорстко заданої мапи на бекенді.

При цьому Registry не вимагає, щоб усі описи блоків мали налаштування відображення. Варіанти й додаткові параметри додаються лише для тих реалізацій, які підтримують відповідний інтерфейс.

Що потрібно змінити, щоб додати новий block

  1. Додати нове значення до ContentBlockTypeEnum.

  2. Створити опис блока, який реалізує спільний контракт.

  3. Визначити в ньому початкові дані, правила валідації та назви полів.

  4. За потреби реалізувати інтерфейс для налаштувань відображення або обмеження області використання.

  5. Зареєструвати опис блока в єдиній точці конфігурації залежностей.

  6. Додати редактор у frontend і публічний renderer для нового типу.

Частини backend, які вже працюють через Registry, не потребують нового case для кожного нового типу блока.

Принцип Open/Closed тут не означає, що наявні файли взагалі не можна змінювати. Service Provider усе одно потрібно доповнити реєстрацією нового блока. Важливіше інше: частини системи, які використовують Content Blocks, не потрібно переписувати щоразу при появі нового конкретного типу.

Registry не те саме, що Factory

Цю різницю важливо чітко зберегти, особливо в серії про патерни проєктування.

Registry зберігає відомий набір реалізацій і дає змогу отримати потрібну за певним ключем.

Factory відповідає за створення або вибір об’єкта, який потрібно повернути коду, що його викликає.

У Content Blocks Registry повертає опис блока за його типом. А в окремій наступній статті про Communication я розберу Factory вже на іншому реальному прикладі.

Що Registry не вирішує

Проблема

Що потрібно окремо

Дублювання типів

Тест або перевірка під час реєстрації.

Відображення на frontend

Окремий frontend Registry або мапа компонентів.

Невідомий тип

Явний виняток або помилка валідації.

Бізнес-логіка конкретного блока

Залишається всередині його опису або відповідного сервісу.

Порядок реєстрації

Має бути однозначним і передбачуваним.

Коли Registry був би зайвим

Якби система мала лише два стабільні типи блоків і не передбачала подальшого розширення, Registry додав би більше структури, ніж реальної користі.

У ICanUp він став виправданим через реальне зростання системи: велику кількість описів блоків, різні правила валідації, метадані для відображення, обмеження підтримки для окремих моделей і кілька частин системи, які працюють з одним набором типів.

Цінність цього патерна не в кількості класів, а в тому, що системі постійно потрібно знаходити відповідну реалізацію за стабільним ключем.

Registry Pattern став корисним у той момент, коли питання "який це тип?" почало повторюватися в різних частинах системи.

Висновок

Моя реалізація Registry Pattern у ICanUp насправді дуже невелика: набір описів блоків, get(), серіалізація та перевірка підтримки.

Але саме ця невелика абстракція дозволила Page Builder рости без великого центрального switch, який мав би знати внутрішню структуру кожного Content Block.

Кожен опис блока відповідає за власний контракт. Registry знає, як знайти потрібний опис. А частини системи, які працюють із блоками, знають лише про Registry та спільний інтерфейс.

Для мене саме так і виглядає корисний патерн проєктування: не додатковий шар заради самої архітектури, а стабільна точка розширення, яка зменшує кількість місць, що доводиться змінювати одночасно.