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

Sohag HasanSohag Hasan
5 min read

Single Responsibility Principle in Laravel

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.

What is the Single Responsibility Principle?

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.

The Wrong Way: A Kitchen Nightmare

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.

The Right Way: A Well-Organized Kitchen

Let's refactor this code following SRP:

  1. First, create a dedicated UserRegistrationService:
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;
    }
}
  1. Create a UserRepository for database operations:
class UserRepository
{
    public function create(array $userData): User
    {
        return User::create([
            'name' => $userData['name'],
            'email' => $userData['email'],
            'password' => bcrypt($userData['password'])
        ]);
    }
}
  1. Create a ProfileService:
class ProfileService
{
    public function createForUser(User $user): Profile
    {
        return Profile::create([
            'user_id' => $user->id,
            'avatar' => 'default.jpg',
            'bio' => ''
        ]);
    }
}
  1. Create an ActivityLogger:
class ActivityLogger
{
    public function logRegistration(User $user): void
    {
        Activity::create([
            'user_id' => $user->id,
            'type' => 'registration',
            'description' => 'User registered successfully'
        ]);
    }
}
  1. Create a MailService:
class MailService
{
    public function sendWelcomeEmail(User $user): void
    {
        Mail::to($user->email)->send(new WelcomeEmail($user));
    }
}
  1. Finally, a clean UserController:
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']);
    }
}

Benefits of Following SRP

  1. Maintainability: Like having specialized chefs for different cuisines, each class has a specific responsibility, making it easier to maintain and modify.

  2. Testability: You can test each component independently, just like you can evaluate each kitchen staff member's performance separately.

  3. Reusability: Services can be reused across different parts of your application, like how a sauce chef's preparations can be used in multiple dishes.

  4. Scalability: Easy to add new features or modify existing ones without affecting other parts of the code.

  5. Better Organization: Code is more organized and easier to understand, like a well-organized kitchen where everything has its place.

Potential Drawbacks and How to Handle Them

  1. More Files: You'll have more classes and files to manage. Solution: Use proper folder structure and naming conventions.

  2. Initial Setup Time: It takes longer to set up initially. Solution: The time invested pays off in long-term maintenance.

  3. Complexity in Service Container: More classes mean more bindings in Laravel's service container. Solution: Use service providers effectively.

Real-World Analogy: The Restaurant Kitchen

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

When to Apply SRP

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

When Not to Over-Apply SRP

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.

Conclusion

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! ๐Ÿš€

1
Subscribe to my newsletter

Read articles from Sohag Hasan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sohag Hasan
Sohag Hasan

WhoAmI => notes.sohag.pro/author