Архитектура в 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/

Опубликовано в категории: Статьи