Сегодня поговорим о теме, где нет универсальных решений, но есть проверенные практики — как организовать код в Laravel, чтобы он оставался чистым даже спустя годы развития.
Я разберу:
-
Почему стандартная структура Laravel быстро превращается в «свалку кода».
-
Как избежать толстых контроллеров и божественных моделей.
-
Какие архитектурные подходы работают для проектов разного масштаба.
Буду рад дискуссии в комментариях — делитесь своим опытом, где я не прав или что можно добавить!
Проблемы базовой архитектуры Laravel
Представьте: вы только начали изучать Laravel и пишете первый проект. Логика — в контроллерах или прямо в роутах, а модели постепенно обрастают методами вроде sendEmailAndUpdateStatistics(). Код работает, но…
Что не так с этим подходом?
-
Нарушение SRP (Single Responsibility Principle)
Контроллер должен только:-
Принять запрос.
-
Вернуть ответ.
Всё! Но когда он валидирует данные, вызывает почтовые сервисы и пишет в БД — это уже 5+ ответственностей в одном классе.
-
-
Сложность повторного использования
Запрос User::where(‘is_active’, true)->get() в 15 контроллерах? При изменении логики придется править все 15 мест. -
Тестирование превращается в ад
Как протестировать метод контроллера, который:-
Создает заказ.
-
Меняет баланс пользователя.
-
Отправляет 3 типа уведомлений?
-
-
Модели — не свалка для методов
Когда в 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 и превращается в «мусорный бак» для кода.
Как должно быть?
-
Один сервис — одна зона ответственности
-
OrderService — только работа с заказами
-
PaymentService — только платежи
-
NotificationService — только уведомления
-
-
Сервис — это координатор, а не исполнитель
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
Проблемы такого подхода:
-
Потеря контекста
Чтобы понять всю логику работы с пользователями, нужно прыгать между 7+ файлами в разных директориях -
Сложность рефакторинга
Изменение поля в модели User требует проверки всех мест, где оно используется — а это минимум 5-10 файлов -
Дублирование зависимостей
Одинаковые сервисы инжектятся в контроллеры, 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/