DEV Community

Cover image for SOLID Principles in PHP and Laravel: First Part (SRP or Single Responsibility Principle)
Alenoosh Baghumian
Alenoosh Baghumian

Posted on

SOLID Principles in PHP and Laravel: First Part (SRP or Single Responsibility Principle)

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,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
        ]);
    }
Enter fullscreen mode Exit fullscreen mode

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)