The single-responsibility principle (SRP) states that there should never be more than one reason for a class to change. In other words, every class should have only one responsibility. A module should be responsible to one, and only one, actor. The term actor refers to a group (consisting of one or more stakeholders or users) that requires a change in the module.
In simple terms: Do one thing, and do it well.
The Real-World Example: Imagine you are hiring a chef for a restaurant.
- Bad Design (The "Do-Everything" Chef): You hire a chef who must cook the food, wash the dishes, calculate the restaurant's taxes, and repair the plumbing when a pipe bursts.
- The Problem: If the tax laws change, your chef is busy studying accounting instead of cooking. If they mess up the plumbing, the kitchen floods, and the entire restaurant shuts down. One change ruins everything.
- Good Design (SRP applied): You hire a chef to cook, a dishwasher to clean, an accountant for taxes, and a plumber for repairs.
- The Benefit: If tax laws change, only the accountant changes their workflow. The chef keeps cooking without interruption.
❌ Wrong Approach: A class with too many jobs
This User class handles user data, database connections, and email delivery. It has three reasons to change.
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
// Job 1: Manage User Data
public function getName() { return $this->name; }
// Job 2: Database Operations (Reason to change: Switching database types)
public function saveToDatabase() {
echo "Saving user to database...\n";
}
// Job 3: Notification Logic (Reason to change: Switching from Email to SMS)
public function sendWelcomeEmail() {
echo "Sending email to " . $this->email . "\n";
}
}
✅ Right Approach: Splitting responsibilities
We break the big class into three tiny, specialized classes. Each has exactly one job.
// 1. Responsible ONLY for user data representation
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
public function getName() { return $this->name; }
public function getEmail() { return $this->email; }
}
// 2. Responsible ONLY for saving data
class UserRepository {
public function save(User $user) {
echo "Saving " . $user->getName() . " to the database.\n";
}
}
// 3. Responsible ONLY for sending messages
class EmailService {
public function sendWelcome(User $user) {
echo "Sending email to " . $user->getEmail() . "\n";
}
}
Why this helps you
- Maintainability: When classes have a single, well-defined responsibility, they're easier to understand and modify.
- Testability: It's easier to write unit tests for classes with a single focus.
- Flexibility: Changes to one responsibility don't affect unrelated parts of the system.
- Reusability: You can use the EmailService to send emails for orders, resets, or other features.
The 3 Main Confusions Cleared Up for the SRP implementation
1. Confusion: "Does 'one responsibility' mean a class can only have one method?"
- Reality: No. A responsibility is a cohesive business role, not a single action.
- Example: A UserRepository class can have save(), delete(), findById(), and update(). This is still one responsibility: managing user data storage.
2. Confusion: "What exactly defines a 'reason to change'?"
- Reality: Robert C. Martin (the creator of SOLID) clarified that a "reason to change" refers to people, not technology. A responsibility is defined by who requests the change (the actors).
- Example: If the Accounting department asks for a change, and the HR department asks for a change, those are two different actors. They should never share the same PHP class.
3. Confusion: "When do I stop splitting classes?"
- Reality: Engineers often split code prematurely, creating UserEmailValidator, UserPasswordValidator, and UserAgeValidator before they even need them. This causes "analysis paralysis."
The 3-Step Decision Matrix (How to Choose)
When looking at a PHP class, ask yourself these three strict questions to decide if you need to split it:
Is this class serving more than one business department/actor?
START SRP_Decision_Process
IF Class_Serves_Multiple_Departments_Or_Actors IS True THEN
EXECUTE Split_Class_Immediately
ELSE
IF Class_Mixes_Different_Technical_Layers IS True THEN
EXECUTE Split_Class
ELSE
EXECUTE Leave_It_Alone
END IF
END IF
END SRP_Decision_Process
- Who is asking for this feature? If a bug fix for the marketing team's email layout can accidentally break the accounting team's invoice calculation, your class has too many responsibilities.
- Are different technical layers mixed? If a single PHP class contains raw SQL queries (SELECT *), HTML markup (
<div>), and validation logic (if (empty)), it violates SRP. Split them. - Can I describe the class without using the word "AND"?
- Bad: "This class holds user data and saves it to MySQL and sends a Slack alert." (3 responsibilities)
- Good: "This class sends Slack alerts." (1 responsibility)
Real-World Decision Rule: "Don't Split Until It Hurts"
To avoid over-engineering, follow the Tactical SRP Rule:
- Start by writing your code in a single class if the feature is small.
- The moment you need to reuse a piece of it (like using the email logic somewhere else), extract it into its own class.
- The moment a class grows past 200 lines of code, audit it using the decision matrix above.
The Laravel Blueprint: Mastering SRP
Let's look at a classic Laravel mistake: putting user registration logic, database insertion, profile photo manipulation, and welcome notifications all inside a single Controller method.
❌ The Junior Approach: The "Do-Everything" Controller (Violates SRP)
This controller method has four distinct reasons to change: database changes, validation rules, image optimization tweaks, and notification carrier changes.
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Mail;
class RegisterController extends Controller
{
public function store(Request $request)
{
// Reason to change 1: Validation rules change
$request->validate([
'name' => 'required',
'email' => 'required|email|unique:users',
'avatar' => 'required|image'
]);
// Reason to change 2: Handling image manipulation
$avatar = $request->file('avatar');
$filename = time() . '.' . $avatar->getClientOriginalExtension();
Image::make($avatar)->resize(300, 300)->save(public_path('/uploads/' . $filename));
// Reason to change 3: Database architecture updates
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'avatar' => $filename,
]);
// Reason to change 4: Switching from Email to Slack/SMS
Mail::to($user->email)->send(new \App\Mail\WelcomeMail($user));
return response()->json(['message' => 'User created successfully!'], 201);
}
}
The Laravel Mastery Approach (Adheres to SRP)
To build this like a framework architect, we segregate the tasks using dedicated Laravel features: Form Requests, Service Classes, and Events/Listeners.
Step 1: Handle Validation via Form Request
The controller doesn't need to know the validation rules. We delegate this to a dedicated request file.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'avatar' => 'required|image|max:2048'
];
}
}
Step 2: Extract Core Logic into a Action/Service Class
We create a dedicated PHP class whose only job is to orchestrate user registration and file handling.
namespace App\Actions;
use App\Models\User;
use App\Events\UserRegistered;
use Illuminate\Http\UploadedFile;
class RegisterNewUser
{
public function execute(array $data, UploadedFile $avatar): User
{
// Handle the file upload logic cleanly
$path = $avatar->store('avatars', 'public');
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'avatar' => $path,
]);
// Fire an event! This class doesn't care HOW notifications are sent.
event(new UserRegistered($user));
return $user;
}
}
Step 3: Handle Communications via Events & Listeners
When the user is saved, an event fires. A dedicated Listener catches it and sends the mail asynchronously.
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeNotification implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
// This class only changes if the notification delivery method changes
Mail::to($event->user->email)->send(new WelcomeMail($event->user));
}
}
Step 4: The Clean, Slim Master Controller
Now look at the controller. It has exactly one responsibility: Receive the HTTP request and return an HTTP response. It is highly readable and closed to external bugs.
namespace App\Http\Controllers;
use App\Http\Requests\RegisterRequest;
use App\Actions\RegisterNewUser;
class RegisterController extends Controller
{
public function store(RegisterRequest $request, RegisterNewUser $registerAction)
{
// Execution is offloaded to specialized workers
$user = $registerAction->execute(
$request->validated(),
$request->file('avatar')
);
return response()->json(['user' => $user], 201);
}
}
Why this makes you a Laravel Master
- Testing Simplicity: You can write a unit test for RegisterNewUser by passing a fake uploaded file without ever rendering an HTTP router or acting like a web browser.
- Impeccable Reusability: If you need to register a user via an API Endpoint, a Blade Web Form, or a Terminal Artisan Command, you simply inject the RegisterNewUser action class and run it. You never have to duplicate code.
Top comments (0)