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

«Куда следует выносить логику из контроллера в Laravel для соблюдения принципа тонких контроллеров?» — вопрос из категории Laravel, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В 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-ответ. Это делает код контроллеров предсказуемым, тестируемым и легким для поддержки.