Сервисный слой в Laravel: куда уносить бизнес-логику
В прошлой статье мы остановились на проблеме: как только в проекте появляется бизнес-логика сложнее «достать и отдать», контроллеры и модели начинают пухнуть. Регистрация пользователя — это не просто User::create(). Это валидация, создание записи, отправка письма, запись в CRM, событие в очередь, отчёт в метрики.
Если всё это в контроллере — невозможно переиспользовать (например, ту же регистрацию из консольной команды или job-а). Если всё в модели — модель превращается в монстра.
Что такое сервисный слой
Service-слой — это набор классов, которые инкапсулируют бизнес-операции. Не «сервис на каждый CRUD», а сервис на сценарий: RegisterUserService, IssuePolicyService, SendSmsService.
Сервис ничего не знает про HTTP. Он принимает данные, выполняет операцию и возвращает результат. Его можно вызвать из контроллера, из консольной команды, из job-а в очереди — везде одинаково.
Как выглядит на практике
class RegisterUserService
{
public function __construct(
private CrmClient $crm,
private Dispatcher $events,
) {}
public function handle(RegisterUserData $data): User
{
return DB::transaction(function () use ($data) {
$user = User::create($data->toArray());
$this->crm->syncContact($user);
$this->events->dispatch(new UserRegistered($user));
return $user;
});
}
}
Контроллер становится плоским:
public function store(RegisterUserRequest $request, RegisterUserService $service)
{
$user = $service->handle(RegisterUserData::fromRequest($request));
return redirect()->route('dashboard');
}
Что даёт такой подход
- Переиспользование. Тот же сервис вызываем из API, из artisan-команды, из очереди.
- Тесты. Сервис — обычный класс, мокаем зависимости через конструктор, тестируем без HTTP.
- Читаемость. Контроллер за 30 секунд показывает, что делает endpoint.
- Транзакции в одном месте. Бизнес-операция атомарна.
Чего стоит избегать
- «Сервис на каждую модель».
UserServiceсо 40 методами — это та же толстая модель, только сбоку. Сервис — это сценарий, а не CRUD-обёртка. - Сервисы, зовущие сервисы по цепочке. Если граф вызовов глубже двух уровней — пора подумать про доменные события или отдельный use-case.
- Передача
Requestвнутрь сервиса. Сервис не должен знать про HTTP. Передавайте DTO или массив.
Запомнить
- Сервис = бизнес-сценарий, а не «обёртка над моделью».
- Один публичный метод (
handle,execute) — нормально и даже желательно. - Зависимости через конструктор, никаких
app()и фасадов внутри. - Транзакции, события и интеграции — внутри сервиса, не в контроллере.
Также может быть интересно
MVC: что это такое и зачем
Разбираем базовый архитектурный паттерн, на котором стоят Laravel, Symfony и большинство современных веб-фреймворков.
Читать далее →