SRP says a class should have one reason to change. That does not mean one method or one line. It means one job. If a database change, a new JSON format, and a new validation rule all force you to edit the same class, that class has too many responsibilities.
The Problem
Consider the following class :
<?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,
]);
}
}
The controller above has two jobs: handling the HTTP request, and writing to the database by creating a wallet when one was missing.
The fix: one job per class
1. The controller handles only 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,
]);
}
}
The controller now reads the request, gets the wallet from a service, and returns the response. Nothing else.
2. A service owns the business rules
<?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 gets the user's wallet and creates an empty wallet if no wallet exists.
A Quick Test
Describe the class in one sentence without the word "and". "This controller returns the user's balance" passes. "This controller validates the transfer and updates the wallets and formats the response" fails.
Conclusion
With these responsibilities separated, the controllers stay small, the wallet rules live in one place, and each part can be tested on its own. How do you apply SRP in your Laravel projects?
Top comments (0)