Куда следует выносить логику из контроллера в Laravel для соблюдения принципа тонких контроллеров?

Ответ

В Laravel я строго следую принципу "тонких контроллеров" и выношу логику в специализированные классы. Вот основные места и примеры из моего опыта:

1. Сервисные классы (Service Classes) — для бизнес-логики. Размещаю в app/Services/. Контроллер делегирует им основную работу.

Пример: Сервис для регистрации пользователя.

// app/Services/UserRegistrationService.php
namespace AppServices;

use AppModelsUser;
use AppNotificationsWelcomeNotification;

class UserRegistrationService
{
    public function register(array $data): User
    {
        // Вся бизнес-логика создания пользователя
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);

        // Отправка welcome-email через очередь
        $user->notify(new WelcomeNotification());

        // Логирование события
        Log::info('New user registered', ['user_id' => $user->id]);

        return $user;
    }
}

// В контроллере
public function store(RegisterRequest $request, UserRegistrationService $service)
{
    $user = $service->register($request->validated());
    return response()->json($user, 201);
}

2. Action-классы или Jobs — для отдельных операций. Идеально для инкапсуляции сложной логики, которую можно повторно использовать.

php artisan make:action/CreateNewOrderAction

3. Form Request Validation — для валидации. Выношу правила валидации из контроллера в отдельные классы app/Http/Requests/.

4. Репозитории — для абстракции работы с данными. Использую, когда нужен сложный доступ к данным или замена источника (БД, API, кэш).

// app/Repositories/UserRepository.php
class UserRepository
{
    public function findActiveWithLatestOrder(int $userId): ?User
    {
        return User::where('id', $userId)
                   ->where('is_active', true)
                   ->with(['latestOrder'])
                   ->first();
    }
}

5. Events & Listeners — для реактивной логики. Например, при создании заказа (OrderCreated) запускаются слушатели на отправку чека, обновление инвентаря и нотификацию менеджера.

Итог: Мой контроллер становится маршрутизатором (router), который:

  1. Валидирует входящий запрос (через Form Request).
  2. Вызывает нужный сервис, action или job.
  3. Возвращает HTTP-ответ. Это делает код контроллеров предсказуемым, тестируемым и легким для поддержки.

Ответ 18+ 🔞

Э, слушай, а ведь это правильный подход, ёпта! Тонкие контроллеры — это как раз то, что доктор прописал, когда проект начинает жрать оперативку и терпения ноль ебать. Сам так делаю, иначе потом разгребать этот пиздец — одно мучение.

Вот смотри, куда я обычно всю эту логику выношу, чтобы контроллер был просто красивой обёрткой, а не монстром на 300 строк.

1. Сервисы (Service Classes) — для всей этой вашей бизнес-хуйни. Кидаю их в app/Services/. Контроллеру остаётся только позвать нужного мужика и отдать результат.

Пример: Сервис, который юзера регистрирует.

// app/Services/UserRegistrationService.php
namespace AppServices;

use AppModelsUser;
use AppNotificationsWelcomeNotification;

class UserRegistrationService
{
    public function register(array $data): User
    {
        // Вся основная движуха по созданию пользователя тут
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);

        // Welcome-email в очередь, чтобы не тормозить
        $user->notify(new WelcomeNotification());

        // Логируем, что вот, мол, новый лох появился
        Log::info('New user registered', ['user_id' => $user->id]);

        return $user;
    }
}

// А в контроллере — красота, одна строчка по сути
public function store(RegisterRequest $request, UserRegistrationService $service)
{
    $user = $service->register($request->validated());
    return response()->json($user, 201);
}

2. Action-классы или Jobs — для чётко выделенных действий. Идеально, когда у тебя есть какая-то хитрая жопа операция, которую можно переиспользовать. Создаёшь — и потом тыкаешь, где надо.

php artisan make:action/CreateNewOrderAction

3. Form Request Validation — чтобы не засирать контроллер проверками. Выносишь все эти $request->validate([...]) в отдельные классы в app/Http/Requests/. Контроллер сразу получает уже провалидированные и чистые данные, доверия ебать ноль к вводу, но зато удобно.

4. Репозитории — когда работа с данными становится сложнее, чем «найди по id». Если тебе нужно не просто User::find(1), а с кучей условий, джойнов и чтобы ещё и из кэша доставалось — это сюда.

// app/Repositories/UserRepository.php
class UserRepository
{
    public function findActiveWithLatestOrder(int $userId): ?User
    {
        return User::where('id', $userId)
                   ->where('is_active', true)
                   ->with(['latestOrder'])
                   ->first();
    }
}

5. Events & Listeners — когда нужно, чтобы одно действие запускало кучу других. Создал заказ (OrderCreated) — и поехало: слушатель на отправку чека, слушатель на списание остатков, слушатель, который бухгалтерию в телеге материт. Красота, всё развязано.

Итог-то какой? Мой контроллер превращается в этакого диспетчера, который:

  1. Говорит: «Данные валидны? А, Form Request уже проверил. Окей».
  2. Кричит в нужную сторону: «Эй, UserRegistrationService, сделай тут че надо!».
  3. Получает результат и отдаёт его наружу в виде JSON или вьюхи.

Получается чисто, понятно и тестируется на ура. А иначе потом сам от себя охуеешь, когда через полгода придётся в этот контроллер-монстр лезть.