Сегодня поговорим о теме, где нет универсальных решений, но есть проверенные практики — как организовать код в 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/
Добавить комментарий