SRP (принцип единственной ответственности) гласит, что у класса должна быть только одна причина для изменения. Это не означает один метод или одну строку кода. Это означает одну задачу. Если изменение базы данных, новый формат JSON и новое правило валидации заставляют вас редактировать один и тот же класс, значит, у этого класса слишком много обязанностей.

 Проблема

Рассмотрим следующий класс:

 <?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Wallet;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class WalletBalanceController extends Controller
{
    /**
     * Return the authenticated user's current balance.
     */
    public function __invoke(Request $request): JsonResponse
    {
        $wallet = $request->user()->wallet()->firstOrCreate([], [
            'balance' => 0,
            'currency' => Wallet::CURRENCY,
        ]);

        return ApiResponse::make([
            'balance' => $wallet->balance,
            'currency' => $wallet->currency,
        ]);
    }
}
 

У контроллера выше две задачи: обработка HTTP-запроса и запись в базу данных путем создания кошелька, если он отсутствовал.

 Решение: одна задача на класс

 1. Контроллер обрабатывает только HTTP

 <?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Wallet;
use App\Services\WalletService;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class WalletBalanceController extends Controller
{
    /**
     * Return the authenticated user's current balance.
     */
    public function __invoke(Request $request, WalletService $wallets): JsonResponse
    {
        $wallet = $wallets->findOrCreate($request->user());

        return ApiResponse::make([
            'balance' => $wallet->balance,
            'currency' => $wallet->currency ?? Wallet::CURRENCY,
        ]);
    }
}
 

Теперь контроллер только считывает запрос, получает кошелек из сервиса и возвращает ответ. Ничего лишнего.

 2. Сервис отвечает за бизнес-логику

 <?php

namespace App\Services;

use App\Exceptions\InsufficientFundsException;
use App\Models\LedgerEntry;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class WalletService
{
    /**
     * Return the user's wallet, creating an empty rial wallet when one does not exist.
     */
    public function findOrCreate(User $user): Wallet
    {
        return $user->wallet()->firstOrCreate([], [
            'balance' => 0,
            'currency' => Wallet::CURRENCY,
        ]);
    }
 

WalletService получает кошелек пользователя и создает пустой кошелек, если он еще не существует.

 Быстрый тест

Опишите класс одним предложением без союза «и». Вариант «Этот контроллер возвращает баланс пользователя» проходит проверку. Вариант «Этот контроллер валидирует перевод, и обновляет кошельки, и форматирует ответ» — нет.

 Заключение

Благодаря разделению этих обязанностей контроллеры остаются компактными, правила работы с кошельками находятся в одном месте, а каждую часть можно протестировать отдельно. А как вы применяете SRP в своих проектах на Laravel?