Динамическое построение меню в модульном 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 с его мощной системой событий и сервис-контейнером идеально подходит для реализации подобных архитектурных паттернов.

Laravel: готовим новый проект. ч1 Docker 

В одной из предыдущих статей мы готовили сам фреймворк к разработке проекта здесь же будет речь о полной подготовке – докер, бэк-энд и фронт-энд(поскольку Laravel это уже full-stack фреймворк).

Докер

Docker – это программная платформа для быстрой разработки, тестирования и развертывания приложений. Docker упаковывает ПО в стандартизованные блоки, которые называются контейнерами.

Зачем он нам вообще нужен?

Как и многие, я начинал веб-разработку на локальном сервере. Однако потом появился Docker. Скажу что долго к нему присматривался и не понимал как к нему подойти. Так было до того пока я не попал на проект где его использовали. И скажу – это удобно!

Так да – зачем? Как минимум – удобная замена локальному веб-серверу – получил/собрал конфиг и пользуешься. Конфиг – композиция докера содержит описание всего окружения в котором работает приложение. К примеру, описывает версию php, его модулей, начальные команды. И так применительно ко всему- от сервера до composer`а. Таким образом, достаточно построить образ из композиции и запустить его в виде контейнера. Еще преимущество – работа над несколькими проектами с разным требованиями к проекту – просто остановил один контейнер и запустил другой.

docker-compose.yaml

Этот файл и есть нашей композицией(конфигом), описывающей несколько образов для проекта. Начнем проект с того что создадим его и заполним. Для разработки нам нужны сам php, сервер (NginX) и база данных(MySQL). Вот начальный код такого файла:

version: "3.8"

services:

секция services как раз и описывает наши образы. Начнем с NginX(должен предупредить – отступы здесь значимые символы)

  nginx:
    image: "nginx:stable-alpine"
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./src:/var/www/laravel

мы взяли стандартный образ сервера, потом сказали на каком порту хотим получать доступ к какому порту в контейнере(конкретно здесь – открытие в браузере). В последней секции(volumes) первая строка указывает на конфиг в путь на компе: формате путь в контейнере. Пример nginx.conf

server {
    listen 80;
    index index.php index.html;
    server_name localhost;
    root /var/www/laravel/public;
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Вторая строка указывает на директорию приложения в том же формате.

Затем построим php

  php:
    build:
      context: dockerFiles
      dockerfile: php.Dockerfile
    volumes:
      - ./src:/var/www/laravel

Здесь мы строим не только контейнер но и сам php; инструкции для него лежат в dockerFiles/php.Dockerfile. Вот его содержимое

FROM php:8.2-fpm-alpine

WORKDIR var/www/laravel

RUN docker-php-ext-install pdo pdo_mysql

скачиваем стандартный образ языка с докер-хаба, назначаем рабочую директорию и отдаем команду включить расширение для работы с MySQL.

Ну и секция с MySQL

  database:
    image: mysql:8.0
    env_file:
      - env/mysql.env
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - "3306:3306"

Все уже знакомо кроме env_file – это файл с переменными для построения БД в контейнере, вот его содержимое

MYSQL_DATABASE=laravel_db
MYSQL_USERNAME=db_user
MYSQL_PASSWORD=root
MYSQL_ROOT_PASSWORD=root2

значения можно изменить, но поля нет.

Конфиг есть, что дальше?

А дальше, по конфигу строим образы

docker-compose build

и запускаем контейнеры

docker-compose up

Только прежде нужно установить докер. Для Windows и MacOS есть графические интерфейсы.

Заключение

Вступительно-ознакомительная часть с докером окончена. Далее немного углубим знакомство с докером и соберем приложение Ларавел.

Моя сборка для разработки Laravel проектов

Приветствую! Хочу рассказать о моих “подготовительных работах” перед реализацией Laravel проектов. Здесь я изложу и кратко опишу почему использую некоторые пакеты. Некоторые я использую во всех проектах, другие – когда сочту нужным. Эта статья – мой личный опыт, может кто-нибудь знает лучшие альтернативы или что еще нужно для более удобной разработки – пишите в комментах – будем благодарны(я и читатели блога). В любом случаи эта статья будет полезна новичкам в данном фреймворке. Погнали!11

Debugbar for Laravel

Этот пакет нужен исключительно для удобства разработки(именно удобства тк я обходился без него поначалу). После его установки, при условии что в .env-файле

APP_DEBUG=true

внизу страницы будет панель

Debugbar for Laravel
debug-bar

с информацией об ошибках, шаблонах, роуте и тд… Также, при помощи фасада Log можно писать собственную инфу в панель. И что не мало важно – она отображает и ajax(к API) запросы.

IDE Helper Generator for Laravel

Еще один пакет, того же автора, предназначенный для удобства и скорости разработки. Он генерирует phpDocs, которые анализируются IDE и видны в виде подсказок, что позволяет избежать опечаток и постоянной инспекции других классов. Работает для моделей и фасадов. Прежде чем генерировать доки для моделей, нужно настроить связь с БД и иметь соответствующие таблицы.

Associate users with permissions and roles

Ни одного проекта без разделения но ролям я еще не делал и этот пакет делает это – позволяет пользователю дать роль.

Associate users with permissions and roles
permissions and roles

Эта зависимость содержит возможности создавать роли, давать права а также содержит мидлвары(что это – расскажу позже, но очень важная и крутая вещь). Даже если рядовому пользователю не нужно регистрироваться на сайте, все равно а сайте всегда есть админка с супер-админами, менеджерами и тд.

Laravel-Modules

Пакет, который я не везде использую, но в проектах он помогает организовать модульную архитектуру. Он содержит команды создающие набор папок и файлов(по структуре подобных к структeре каталога app). Как по мне базовая архитектура лучше всех, однако если над проектом работает команда – проекты с базовой превращаются в адок. Обьяснение почему я недолюбливаю модульность: допустим ,есть модуль Account на лице сайта, который имеет модель User; есть модуль Admin, где также есть модель User. Вопрос: как поступить – дублировать код(тк поля почти идентичны), делать общие трейты и фасады или вне модулей делать абстрактную модель User, и наследоваться от нее(нарушая идею модульности в моем понимании)? Возможно , я еще не достаточно опытен и чего то не понимаю, но в базовом исполнении, прибегая к использованию лара-фасадов таких дилемм не возникает.

Easy AdminLTE integration with Laravel

Небольшой пакет для построения админ-панели, содержащий набор шаблонов и стилей, основанных на Bootstrap css. Код самых виджетов можно глянуть в AdminLTE Demo.

Заключение

Это все рекомендации, однако они, я уверен, сделают разработку, и что важнее, развитие проекта проще , быстрее и понятнее…