Архитектура в Laravel. Как сделать код понятным и масштабируемым

от автора

Сегодня поговорим о теме, где нет универсальных решений, но есть проверенные практики — как организовать код в Laravel, чтобы он оставался чистым даже спустя годы развития.

Я разберу:

  • Почему стандартная структура Laravel быстро превращается в «свалку кода».

  • Как избежать толстых контроллеров и божественных моделей.

  • Какие архитектурные подходы работают для проектов разного масштаба.

Буду рад дискуссии в комментариях — делитесь своим опытом, где я не прав или что можно добавить!

Проблемы базовой архитектуры Laravel

Представьте: вы только начали изучать Laravel и пишете первый проект. Логика — в контроллерах или прямо в роутах, а модели постепенно обрастают методами вроде sendEmailAndUpdateStatistics(). Код работает, но…

Что не так с этим подходом?

  1. Нарушение SRP (Single Responsibility Principle)
    Контроллер должен только:

    • Принять запрос.

    • Вернуть ответ.
      Всё! Но когда он валидирует данные, вызывает почтовые сервисы и пишет в БД — это уже 5+ ответственностей в одном классе.

  2. Сложность повторного использования
    Запрос User::where(‘is_active’, true)->get() в 15 контроллерах? При изменении логики придется править все 15 мест.

  3. Тестирование превращается в ад
    Как протестировать метод контроллера, который:

    • Создает заказ.

    • Меняет баланс пользователя.

    • Отправляет 3 типа уведомлений?

  4. Модели — не свалка для методов
    Когда в User.php живут payInvoice(), banWithReason() и generateReport(), вы получаете класс на 2000 строк, который невозможно поддерживать.

Давайте разберем основные паттерны, чтобы этого избежать!

Паттерны красивого кода в Laravel.

Action — простейший паттерн, куда мы выносим небольшой небольшую часть логики

class FilterDataForInsertAction {      public function execute(OrderDTO $data): array   {     // Ваша логика   }    }

Таких экшенов в одном методе может быть несколько, но лучше использовать один экшен, который собирает другие.

Как мы видим, этот класс имеет всего один метод execute (можно сделать __invoke), где мы пишем логику строго по его ответственности. И здесь же мы видим, что в аргументах мы получает OrderDTO.

DTO — Data Transfer Object, буквально объект для передачи данных. Он используется для передачи данных между слоями вашего приложения. Не принято отдавать его клиенту, в основном он нужен как раз для того, чтобы передать данных в Service Layer, Action, Repository и т.д.

class OrderDTO {        public readonly int $id;     public readonly string $name;     public readonly string $description;     public readonly string $phone;          public function __construct(int $id, string $name, string $description, string $phone)      {         $this->id = $id;         $this->name = $name;         $this->description = $description;         $this->phone = $phone;     } }

Обычно в DTO не принято писать какую-либо бизнес логику. Класс DTO должен быть максимально простым.

Repository — это класс, куда мы выносим какую-либо логику работы с базой данных. Чаще он используется для приложений, где есть вероятность смены базы данных.

class OrderRepository implements OrderRepositoryInterface {    public function getAll(): Collection   {     return Order::with()       ->where()       ->groupBy()           ->having()       ->get()   }    }
interface OrderRepositoryInterface {   public function getAll(): Collection  }

Здесь важно подметить, что мы зависим от абстракции в виде интерфейса, а не реализации. Для этого в Laravel есть DI и Service Provider:

class AppServiceProvider extends ServiceProvider { const BINDINGS  = [ OrderRepositoryInterface::class => OrderRepository::class, TaskRepositoryInterface::class => TaskRepository::class ];      public function register(): void     {     foreach (self::BINDINGS as $abstract => $concrete) {     $this->app->bind($abstract, $concrete);     }     }  }

Лучше создавать свои репозитории, где мы связываем именно репозитории, чтобы не загрязнять AppServiceProvider.

В контроллере:

public function OrderController extends Controller {      private OrderRepositoryInterface $orderRepository;    public function __construct(OrderRepositoryInterface $orderRepository) { $this->orderRepository = $orderRepository; }    public function index(): OrderResource   {     return OrderResource::collection($this->orderRepository->getAll())   } }

Service Layer — это обоюдоострый меч в Laravel-разработке. Многие просто переносят проблему из толстых контроллеров в толстые сервисы, создавая новые «божественные классы».

Главная ошибка

class DoEverythingService {     // 500 строк кода, которые делают:     // - работу с БД     // - отправку писем     // - API-запросы     // - генерацию отчетов }

Такой сервис нарушает все принципы SOLID и превращается в «мусорный бак» для кода.

Как должно быть?

  1. Один сервис — одна зона ответственности

    • OrderService — только работа с заказами

    • PaymentService — только платежи

    • NotificationService — только уведомления

  2. Сервис — это координатор, а не исполнитель

class OrderService {       public function __construct(         private readonly CreateOrderAction $createOrder,         private readonly CreatePaymentForOrderAction $createPayment,         private readonly OrderNotifierInterface $notifier,     ) {}      public function createOrder(OrderDTO $data): Order     {         $order = $this->createOrder->execute($data);         $this->createPayment->execute($order);         $this->notifier->send($order);         return $order;     } }

На этом всё? Код красивый?

Даже при идеальном соблюдении всех паттернов Laravel имеет фундаментальную архитектурную проблему — код одной сущности оказывается разбросан по десяткам папок. Рассмотрим на примере работы с пользователями:

Типичная структура Laravel

app/ ├── Http/ │   └── Controllers/ │       └── UserController.php ├── Models/ │   └── User.php ├── Jobs/ │   └── SendWelcomeEmail.php ├── Console/ │   └── Commands/ │       └── DeleteInactiveUsers.php ├── Events/ │   ├── UserRegistered.php │   └── UserDeleted.php └── Listeners/     ├── SendVerificationEmail.php     └── UpdateUserStatistics.php

Проблемы такого подхода:

  1. Потеря контекста
    Чтобы понять всю логику работы с пользователями, нужно прыгать между 7+ файлами в разных директориях

  2. Сложность рефакторинга
    Изменение поля в модели User требует проверки всех мест, где оно используется — а это минимум 5-10 файлов

  3. Дублирование зависимостей
    Одинаковые сервисы инжектятся в контроллеры, jobs и команды

Для избежание таких проблем, в Laravel придумали модульную архитектуру. Структура такого проекта с использованием модулей будет подобной:

└── Users/     ├── Actions/     │   ├── CreateUserAction.php     │   └── DeleteUserAction.php     ├── Data/     │   └── UserDataDTO.php     ├── Events/     ├── Listeners/     ├── Models/     │   └── User.php     ├── Jobs/     │   └── SendWelcomeEmail.php     └── Controllers/         └── UserController.php

Подобную архитектуру можно внедрить и самому, но уже есть готовые решения. Самое классное решение, которое показывает себя не совсем как модульная архитектура (использует другой нейминг), это Porto (apiato) https://github.com/apiato/apiato. Рассказывать о подробной его архитектуре здесь я уже не буду, это займёт слишком много времени, поэтому желаю вам самим в нём разобраться!

Сложная архитектура — не панацея.

Важно понимать, что принципы это лишь рекомендации, а не законы. Если дедлайны горят, сделать нужно проект вчера, или это проект, который в будущем не будет никак расширяться — не нужно с головой уходить в архитектурные паттерны. Для просто CRUD приложения поднимать модульную архитектуру — абсолютная трата времени. Не забывайте о YAGNI — не делайте того, о чём вас не просят. (это не касается тестовых заданий, где нужно показать весь ваш скилл).

Архитектурных паттернов действительно существует великое множество — от простых Actions до сложных CQRS и Event Sourcing систем и великого и ужасного DDD. В этой статье мы разобрали лишь фундаментальные подходы в обозревательном ключе. Это не руководство по паттернам, я лишь хочу рассказать что используется в Laravel, чтобы вы могли углубиться дальше сами!

Делитесь своими находками с коллегами, участвуйте в code review, будьте открыты к новому. Именно так рождается по-настоящему качественный код. Всем спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/898584/