Динамическое построение меню в модульном Laravel-приложении с использованием событий

Модульная архитектура — один из лучших способов поддерживать чистоту и расширяемость кода в больших Laravel-приложениях. Однако с ростом числа модулей часто возникает задача динамического построения пользовательского интерфейса, особенно навигационного меню, которое должно собирать пункты из разных частей системы. В этой статье я покажу элегантный подход к решению этой проблемы с помощью кастомных событий и View composer.

Проблема

В монолитном приложении меню можно просто прописать в шаблоне Blade. Но в модульной системе каждый модуль должен иметь возможность добавить свои пункты в меню, не нарушая работу других модулей. Жёсткая привязка к шаблонам приводит к хрупкости и усложнению поддержки.

Решение: событийный подход

Идея проста: мы создаём специальное событие BuildingMenu, которое собирает пункты меню от всех заинтересованных модулей. Затем через View composer передаём собранное меню в шаблон.

Шаг 1: Класс события BuildingMenu

<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class BuildingMenu
{
    use Dispatchable, SerializesModels;

    private array $items = [];

    public function addItem(array $item): void
    {
        $this->items[] = $item;
    }

    public function getItems(): array
    {
        return $this->items;
    }
}

Класс выполняет две функции:

  • Является событием (через трейты Dispatchable, SerializesModels).
  • Служит контейнером для пунктов меню (методы addItem и getItems).

Обратите внимание: массив $items инициализирован как пустой, чтобы избежать ошибок при отсутствии пунктов.

Шаг 2: View composer для передачи меню в шаблон

<?php

use App\Events\BuildingMenu;
use Illuminate\Support\Facades\View;

View::composer('welcome', function ($view) {
    $buildingMenu = new BuildingMenu();
    event($buildingMenu);

    $view->with('buildingMenu', $buildingMenu);
});

View composer связывается с шаблоном welcome (или любым другим). При каждом рендеринге этого шаблона:

  • Создаётся экземпляр BuildingMenu.
  • Запускается событие event($buildingMenu), которое уведомляет всех слушателей.
  • Объект меню передаётся в шаблон как переменная $buildingMenu.

Шаг 3: Слушатель события, добавляющий пункты

<?php

use App\Events\BuildingMenu;
use Illuminate\Support\Facades\Event;

Event::listen(BuildingMenu::class, function (BuildingMenu $event) {
    $event->addItem([
        'text' => 'Счета',
        'url'  => 'invoices.index',
        'icon' => 'fas fa-file-invoice-dollar',
        'can'  => 'view_invoices', // Проверка прав через Gate
    ]);
});

Это пример слушателя, который может быть размещён в сервис-провайдере модуля. Он добавляет пункт «Счета» с иконкой, маршрутом и проверкой прав (ключ can позволяет скрыть пункт у пользователей без соответствующего разрешения).

Как это работает в модульной системе

Каждый модуль регистрирует своего слушателя в своём сервис-провайдере:

<?php

namespace Modules\Invoices\Providers;

use Illuminate\Support\ServiceProvider;
use App\Events\BuildingMenu;
use Illuminate\Support\Facades\Gate;

class InvoicesServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(BuildingMenu::class, function (BuildingMenu $event) {
            if (Gate::allows('view_invoices')) {
                $event->addItem([
                    'text' => 'Счета',
                    'url'  => 'invoices.index',
                    'icon' => 'fas fa-file-invoice-dollar',
                ]);
            }
        });
    }
}

Таким образом, модуль ничего не знает о других модулях, но вносит свой вклад в общее меню. При добавлении нового модуля нужно лишь зарегистрировать его провайдер в config/app.php — меню автоматически пополнится.

Расширение функциональности

Класс BuildingMenu можно улучшить, добавив:

  • Группировку пунктов (например, выпадающие подменю).
  • Сортировку по приоритету.
  • Кэширование собранного меню (чтобы не строить его при каждом запросе).

Пример с группировкой:

<?php

$event->addItem([
    'text'    => 'Финансы',
    'icon'    => 'fas fa-wallet',
    'submenu' => [
        ['text' => 'Счета', 'url' => 'invoices.index'],
        ['text' => 'Платежи', 'url' => 'payments.index'],
    ],
]);

В шаблоне Blade можно реализовать рекурсивный вывод для поддержки многоуровневого меню.

Достоинства подхода
  • Модульность: каждый модуль управляет своими пунктами независимо.
  • Тестируемость: легко писать тесты для слушателей событий.
  • Гибкость: пункты могут зависеть от прав пользователя, конфигурации, состояния приложения.
  • Чистота шаблонов: в Blade остаётся только простой цикл по пунктам, без логики.
Недостатки
  • Небольшое увеличение сложности (нужно создать событие и View composer).
  • Требуется внимательно следить за производительностью, если пунктов очень много (можно добавить кэширование).

Заключение

Использование событий для построения меню превращает навигацию из статического элемента в динамическую, расширяемую систему. Такой подход особенно полезен в больших приложениях с командной разработкой, где несколько программистов работают над разными модулями одновременно. Он снижает связанность (coupling) и повышает связность (cohesion) кода, следуя принципам SOLID.

Внедрение описанного механизма займёт всего несколько часов, но сэкономит много времени в будущем при добавлении новых модулей и изменении структуры меню. Laravel с его мощной системой событий и сервис-контейнером идеально подходит для реализации подобных архитектурных паттернов.