Laravel: Ваши контроллеры должны выглядеть так

Источник: «Laravel — Your controllers should look like this»
Наведите порядок в контроллерах с помощью сервисных классов

Сценарий

Не так редко, когда начинаешь работать над проектом приходится иметь дело с божественными классами контроллеров. Знаете, что я имею в виду? Кто никогда не сталкивался с контроллером хранящим метод который проверяет, сохраняет, создаёт отношения, отправляет электронные письма, … и возвращает ответ?

Я вижу, что это очень распространённый подход в Laravel-сообществе, в то время как Rails-разработчики склонны делать почти то же самое в классах моделей. Не думаю, что это так уж плохо иметь что-то ещё (помимо обработки запросов и ответов) внутри контроллеров в некоторых конкретных ситуациях, но в целом я бы предпочёл использовать первый принцип SOLID: Принцип единой ответственности (SRP) сделать немного больше сейчас, чтобы упростить задачу в будущем.

Преимущества переноса бизнес-логики из контроллера в новые классы бесчисленны, но напомню три из них:

Давайте посмотрим как применить эту концепцию при рефакторинге контроллера

Для простоты давайте рассмотрим, в котором UserController просто проверяет данные, создаёт нового пользователя, его роли и перенаправляет с сообщением о состоянии:

public function store(Request  $request)
{
# Проверяем данные
$request->validate([
'name' => 'required',
'email' => 'required|unique:users',
'phone_number' => 'required',
]);

# Создаём пользователя
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'phone_number' => $request->phone_number,
]);

# Создаём роли пользователя
if(!empty($request->roles)) {
$user->roles()->sync($request->roles);
}

# Перенаправляем с сообщением о состоянии
return redirect()
->route('user.index')
->with('message','Пользователь успешно создан.');
}

Выглядит нормально, правда? Но мы можем сделать его лучше. Давайте переместим проверку в Form Request, выполнив эту команду:

php artisan make:request StoreUserRequest

Она создаст новый файл в каталоге App\Http\Requests. Я просто разрешаю использование и переношу правила валидации из контроллера в метод rules следующим образом:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/

public function authorize()
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/

public function rules()
{
return [
'name' => 'required',
'email' => 'required|unique:users',
'phone_number' => 'required',
];
}
}

Как использовать этот класс:

Всё, что вам нужно сделать — указать запрос в методе вашего контроллера. Входящий запрос формы проверяется до вызова метода контроллера, значит вам не нужно загромождать контроллер какой-либо логикой проверки.

use App\Http\Requests\StoreUserRequest;


public function store(StoreUserRequest $request)
{
# Создаём пользователя
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'phone_number' => $request->phone_number,
]);

# Создаём роли пользователя
if(!empty($request->roles)) {
$user->roles()->sync($request->roles);
}

# Перенаправляем с сообщением о состоянии
return redirect()
->route('user.index')
->with('message','Пользователь успешно создан.');
}

Немного лучше. Теперь мы создадим новый сервисный класс. Он будет отвечать за создание пользователей.

Поскольку у нас для этого нет специальных команд, нужно будет создать его вручную. Внутри каталога app создадим каталог Service с новы классом: app/Services/UserService.php и перенесём логику из контроллера в этот класс:

<?php
namespace App\Services;

use App\Models\User;
use Illuminate\Http\Request;

class UserService
{
public function createUser(Request $request): User
{
# Создаём пользователя
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'phone_number' => $request->phone_number,
]);

# Создаём роли пользователя
if(!empty($request->roles)) {
$user->roles()->sync($request->roles);
}
return $user;
}
}

Импортируем в контроллер зависимость, которую только что создали:

use App\Services\UserService;

public function store(StoreUserRequest $request, UserService $userService)
{
$userService->createUser($request);

# Перенаправляем с сообщением о состоянии
return redirect()
->route('user.index')
->with('message','Пользователь успешно создан.');
}

Вы заметили разницу?

Да, теперь у нас больше классов и файлов, но с другой стороны, у каждого из них есть свои обязанности, их можно повторно использовать в других частях приложения и индивидуально тестировать.

Не говоря уж о контроллере, который теперь отвечает только за получение запроса, вызов нужного сервиса и возврат ответа.

Красивее, не так ли?

Должен ли я использовать сервисные классы?

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

Это не общее правило, но при создании нового приложения вы должны позаботиться о том, чтобы каждый класс был как можно более чистым, включая ваши контроллеры.

Дополнительные материалы

Предыдущая Статья

PHP: Это DTO или Объект-Значение?

Следующая Статья

Laravel: Использование DTO для сохранения контекста