Keep It Simple: Mastering the Single Responsibility Principle in Laravel with Real-World Examples

WhoAmI => notes.sohag.pro/author
Search for a command to run...

WhoAmI => notes.sohag.pro/author
Open-Closed Principle in Laravel Introduction Have you ever found yourself repeatedly modifying existing code to add new features, only to break something else in the process? The Open-Closed Principle (OCP) is here to save you from this maintenance ...
I had a solid list of reasons my life wasn't moving faster, until a grainy old lecture pointed out the one name missing from it

The finale isn't a victory lap. It's the story of the control I shipped that did nothing, the footgun still sitting in my demo, and the handful of things I'd keep exactly as they are.

How do you hold a large payment for a second pair of eyes without ever letting the unapproved money touch a balance, and how do you stream that decision to the outside world without standing up a broker?

How do you show the total under a parent account when the whole system refuses to store a balance? A recursive query, a trigger that refuses to draw a circle, and a rule about what actually has to sum to zero.

Every time I wanted to change an FX rate I had to edit a file on the server and restart the app. So I moved rates and markup into a live admin API, and then audited it hard enough to find the bug that quietly undid the whole thing.

Have you ever walked into a kitchen where one person is simultaneously cooking, washing dishes, taking phone orders, and managing inventory? Chaos, right? This is exactly what happens in our code when we ignore the Single Responsibility Principle (SRP). Let's break down this fundamental principle using Laravel and real-world examples that you'll encounter in your daily development journey.
The Single Responsibility Principle, part of the SOLID principles, states that a class should have only one reason to change. In simpler terms: A class should do one thing, and do it well. Think of it like specialized workers in a restaurant – you have dedicated chefs, waiters, and dishwashers, each focusing on their specific tasks.
Let's look at a common mistake developers make when creating a User registration system:
class UserController extends Controller
{
public function register(Request $request)
{
// Validate input
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
]);
// Create user
$user = User::create([
'name' => $validatedData['name'],
'email' => $validatedData['email'],
'password' => bcrypt($validatedData['password'])
]);
// Send welcome email
Mail::to($user->email)->send(new WelcomeEmail($user));
// Create user profile
Profile::create([
'user_id' => $user->id,
'avatar' => 'default.jpg',
'bio' => ''
]);
// Log user activity
Activity::create([
'user_id' => $user->id,
'type' => 'registration',
'description' => 'User registered successfully'
]);
return response()->json(['message' => 'Registration successful']);
}
}
This code is like a chef who's doing everything in the kitchen. It's handling validation, user creation, email sending, profile creation, and activity logging. This violates SRP because the class has multiple reasons to change.
Let's refactor this code following SRP:
class UserRegistrationService
{
protected $userRepository;
protected $profileService;
protected $activityLogger;
protected $mailService;
public function __construct(
UserRepository $userRepository,
ProfileService $profileService,
ActivityLogger $activityLogger,
MailService $mailService
) {
$this->userRepository = $userRepository;
$this->profileService = $profileService;
$this->activityLogger = $activityLogger;
$this->mailService = $mailService;
}
public function register(array $userData): User
{
$user = $this->userRepository->create($userData);
$this->profileService->createForUser($user);
$this->mailService->sendWelcomeEmail($user);
$this->activityLogger->logRegistration($user);
return $user;
}
}
class UserRepository
{
public function create(array $userData): User
{
return User::create([
'name' => $userData['name'],
'email' => $userData['email'],
'password' => bcrypt($userData['password'])
]);
}
}
class ProfileService
{
public function createForUser(User $user): Profile
{
return Profile::create([
'user_id' => $user->id,
'avatar' => 'default.jpg',
'bio' => ''
]);
}
}
class ActivityLogger
{
public function logRegistration(User $user): void
{
Activity::create([
'user_id' => $user->id,
'type' => 'registration',
'description' => 'User registered successfully'
]);
}
}
class MailService
{
public function sendWelcomeEmail(User $user): void
{
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}
class UserController extends Controller
{
protected $registrationService;
public function __construct(UserRegistrationService $registrationService)
{
$this->registrationService = $registrationService;
}
public function register(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
]);
$user = $this->registrationService->register($validatedData);
return response()->json(['message' => 'Registration successful']);
}
}
Maintainability: Like having specialized chefs for different cuisines, each class has a specific responsibility, making it easier to maintain and modify.
Testability: You can test each component independently, just like you can evaluate each kitchen staff member's performance separately.
Reusability: Services can be reused across different parts of your application, like how a sauce chef's preparations can be used in multiple dishes.
Scalability: Easy to add new features or modify existing ones without affecting other parts of the code.
Better Organization: Code is more organized and easier to understand, like a well-organized kitchen where everything has its place.
More Files: You'll have more classes and files to manage. Solution: Use proper folder structure and naming conventions.
Initial Setup Time: It takes longer to set up initially. Solution: The time invested pays off in long-term maintenance.
Complexity in Service Container: More classes mean more bindings in Laravel's service container. Solution: Use service providers effectively.
Think of your application like a restaurant kitchen:
UserController: The head chef who oversees operations but doesn't do all the cooking
UserRegistrationService: The kitchen manager coordinating different stations
UserRepository: The pantry chef handling ingredients (data)
ProfileService: The garde manger handling user profiles
ActivityLogger: The kitchen record keeper
MailService: The communication system between kitchen and customers
Apply SRP when your class:
Has methods that serve different purposes
Becomes difficult to test
Changes frequently for different reasons
Has too many dependencies
Exceeds 200-300 lines of code
Don't create separate classes for every tiny operation. For example, if you have a simple validation rule, it doesn't need its own class. Use common sense and consider maintainability as your guide.
The Single Responsibility Principle might seem like extra work initially, but it's like organizing a professional kitchen – everything has its place and purpose. When implemented correctly, it makes your code more maintainable, testable, and scalable. Start applying SRP in your next Laravel project, and you'll see the benefits as your application grows.
Remember: Like a well-organized kitchen produces better food more efficiently, well-organized code following SRP produces better applications more maintainably.
Happy coding! 🚀