Сервисный слой — это архитектурный паттерн, который выносит бизнес-логику из контроллеров в отдельные классы-сервисы. Это помогает:
- Соблюдать принцип единой ответственности (SRP)
- Упростить тестирование
- Уменьшить дублирование кода
- Сделать код более читаемым и поддерживаемым
Главное что стоит понимать - это то, что контроллер нужен лишь для того чтобы принять запрос, передать его куда нужно, дождаться ответа и вернуть ответ. То есть в идеале метод контроллера будет выглядить следующим образом:
class UserController extends Contoller
{
public function get(UserRequest $request): JsonResponse
{
return (new UserGetAction)->handle($request->perPage);
}
}
Контроллеры должны быть тонкими. Представьте, что у вас есть контроллер для управления пользователем, который содержит базовые CRUD операции, так и различные эндпоинты, а если еще и OpenAPI документацию прямо там вести. Если оставлять всю логику в контроллере, то читать его становится крайне сложно, даже если логика не очень большая. Куда проще видеть лишь "содержание" контроллера. Где, аналогично примеру выше, будешь лишь название метода и экшен который его обрабатывает.
Помимо визуальной части такой подход еще имеет другие преимущества, а именно:
- Упрощение тестирования. Можно писать как отдельные тесты на общую работу контроллера, так и детализированные на работу экшена
- Переиспользование логики. При необходимости из другого места приложения можно вызвать нужный экшен и получить результат
- Соблюдение SOLID принципов. Контроллер теперь у нас отвечает только за одно действие (контроль выполнения запроса). При необходимости внесения доработки в логику не нужно лезть в контроллер, а можно расширять экшен
Что такое экшены?
Экшены (Actions) - это классы-обработчики запроса. То есть если нам нужно обработать запрос на получение пользователей (GET /api/users), то мы создадим для него отдельный UserGetAction. Который уже будет вести нужную обработку со всеми фильтрациями.
Пример:
namespace App\Actions;
use App\Repositories\UserRepository;
class GetUsersAction
{
public function __construct(
private UserRepository $userRepository
) {}
public function handle(int $perPage = 15): array
{
return $this->userRepository->getPaginatedUsers($perPage)->toArray();
}
}
Да, логика в экшене лишь в одн строчку, но данная логика может быть и довольно огромной. Главное уловить суть, что Action - обработчик метода, а не контроллер.
Что такое репозитории?
В примере выше видно, что в конструкторе класса был объявлен репозиторий и через него были получены пользователи. Основная суть репозиториев - обработка запросов к базе данных. То есть у нас к примеру есть модель - User. Мы хотим получить пользователей с пагинацией, с какими-либо фильтрами, еще логикой в зависимости от переданных значений, но использовать лишь в одном или парочке мест. Писать большой запрос прямо в экшене будет сильно бить по визуалу, нарушать SOLID принципы. Набивать модель скопами или кастомными методами, то если так сделать на каждый нужный нам экшен, то конца модели мы в дальнейшем так и не увидем. Для этого есть репозитории. Репозитории выносят логику формирования сложного запроса к модели или базе данных в отдельный класс. И опять же если нам нужно будет изменить логику получения пользователей, то не нужно будет лезть в сам экшен и править его.
Простой пример:
namespace App\Repositories;
use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;
class UserRepository
{
public function getPaginatedUsers(int $perPage = 15): LengthAwarePaginator
{
return User::paginate($perPage);
}
public function getUsersWithBalanceAbove(float $amount): array
{
return User::where('balance', '>', $amount)->get()->toArray();
}
}
Что такое сервис?
Последней из составляющих остается - сервис. Сервис выносит общую логику, которая может быть переиспользована несколько раз. Этой логикой может быть:
- Нотификация пользователя
- Расчет его баланса
- Различные аналитики
- Управление JWT токеном
и многое другое.
Пример:
namespace App\Services;
use App\Models\User;
class BalanceService
{
// Бизнес-логика пополнения баланса
public function deposit(User $user, float $amount): array
{
$oldBalance = $user->balance;
$user->increment('balance', $amount);
// Логирование операции
$this->logTransaction($user, 'deposit', $amount, $oldBalance);
// Проверка на достижение нового статуса
$newStatus = $this->checkStatusUpgrade($user);
return [
'old_balance' => $oldBalance,
'new_balance' => $user->balance,
'status' => $newStatus,
'bonus' => $this->calculateBonus($amount)
];
}
// Бизнес-логика списания средств
public function withdraw(User $user, float $amount): bool
{
if ($user->balance < $amount) {
throw new \Exception('Недостаточно средств');
}
$oldBalance = $user->balance;
$user->decrement('balance', $amount);
$this->logTransaction($user, 'withdraw', $amount, $oldBalance);
return true;
}
// Бизнес-правила для бонусов
private function calculateBonus(float $amount): float
{
if ($amount > 1000) return $amount * 0.05; // 5% бонус
if ($amount > 500) return $amount * 0.03; // 3% бонус
return 0;
}
// Бизнес-правила для статусов
private function checkStatusUpgrade(User $user): string
{
if ($user->balance > 5000) return 'vip';
if ($user->balance > 1000) return 'premium';
return 'standard';
}
private function logTransaction(User $user, string $type, float $amount, float $oldBalance): void
{
// Логирование для аудита
\Log::info("Balance {$type}", [
'user_id' => $user->id,
'amount' => $amount,
'old_balance' => $oldBalance,
'new_balance' => $user->balance
]);
}
}