DEV Community

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

Posted on Edited 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 per class 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.

Important Note : SRP does not mean "one method per class"

SRP doesn't mean that every class should contain only one method. A class can have several methods as long as they belong to the same responsibility and have the same reason to change.

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

First let's see what the WalletBalanceController does :

  1. The WalletBalanceController controller is an invokable controller, so Laravel calls __invoke() when this endpoint is requested.

  2. firstOrCreate() method looks for the user's wallet and creates one with a zero balance if it doesn't exist.

  3. ApiResponse is a project-specific helper used here to standarddize API responses.

The WalletBalanceController controller is responsible for handling the HTTP request, but it also contains the wallet's creation rule. If that rule changes, the controller needs to change too.

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)