Single Responsibility Principle in Laravel

A Deep Dive into the Single Responsibility Principle and how to Ensure you Adhere to it in Laravel

Shaun Thornburgh
5 min readOct 17, 2023

I have previously written about SOLID principles in Laravel, but I wanted to take a deeper dive and look into each principle separately as I feel this would be useful. In this article we will take a deep dive in to the first of these principles; Single Responsibility Principle.

Single Responsibility Principle in Laravel
Single Responsibility Principle in Laravel

What is SOLID?

SOLID is a popular set of software design principles that are used in object-oriented software development. Using SOLID helps us as developers to build efficient, maintainable, and scalable software systems. The five principles are:

  • Single Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISG)
  • Dependency Inversion Principle (DIP)

The overall goal of the principles helping developers make changes to their code without creating problems for themselves or other developers working on the project. After all, no one likes owning up to a mistake in a stand up!

Single Responsibility Principle

Have you ever found yourself sifting through a controller that’s longer than the queue at a hipster coffee shop during lunchtime? If so, you’ve probably stumbled upon a codebase that could benefit from the Single Responsibility Principle (SRP). Let’s dive into what SRP is, why it matters, and how to avoid (or fix) its most common violations in Laravel.

What is the Single Responsibility Principle?

In simple terms, SRP states that a class should have only one reason to change. This means that a class should only do one thing. Remember that friend who claims they can multitask by watching TV, eating pizza, and coding simultaneously? Well, unlike them, classes should stick to one task to perform it well.

Advantages of SRP in Laravel:

  1. Maintainability: Smaller, focused classes are easier to understand, update, and debug. Imagine trying to find a bug in a 1,000-line controller vs. a 100-line one. The math is simple!
  2. Reusability: When classes have a single responsibility, they’re more likely to be useful in multiple places. It’s like that versatile black t-shirt you wear on both casual Fridays and dates.
  3. Testability: Single-responsibility classes are more predictable, making them easier to test. Ever tried unit testing a monolithic class? It’s like trying to herd cats.

Classic Violations of SRP in Laravel (and How to Fix Them):

Bloated Controller Functions:

Violation: The store method is validating the request, creating the record in the database, and sending a response.

class RegisterController extends Controller
{
protected function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255'],
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed'
]);

$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);

return response()->json(['user' => $user], Response::HTTP_CREATED);
}
}

In this example you can see that the store method is doing lots of things, or has more than a single responsibility.

Resolution: Remove code from the controller into smaller classes.

First of all we can move the validation to it’s own class.

class StoreUserRequest extends FormRequest
{
public function authorize()
{
return true;
}

public function rules()
{
return [
'name' => 'required|string|max:255'],
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed'
];
}

public function data(): RegisterUserDTO
{
return new RegisterUserDTO(
$this->validated('name'),
$this->validated('email'),
$this->validated('password'),
);
}
}

Next we can move the creation of the database record to a repository class.

class UserRepository implements UserRepositoryInterface
{
public function create(RegisterUserDTO $userDTO): User
{
return User::create([
'name' => $userDTO->getName(),
'email' => $userDTO->getEmail(),
'password' => Hash::make($userDTO->getPassword())
]);
}
}

We can now remove all the unnecessary code from the controller:

class RegisterController extends Controller
{
protected function store(StoreUserRequest $request)
{
$user = $userRepository->create($request->data())

return response()->json(['user' => $user], Response::HTTP_CREATED);
}
}

The Overstuffed Controller:

Violation: This controller is handling CRUD operations, handling profile image uploads, and sending messages? Someone give this controller a vacation!

class UserController extends Controller {
public function create() { /*...*/ }
public function store() { /*...*/ }
public function profileImageUpload() { /*...*/ }
public function sendMessage() { /*...*/ }
// And the list goes on...
}

Resolution: Break the controller into smaller, focused controllers. Maybe ProfileImageController for the profile image and MessageController for the messaging logic. Laravel allows us to use “Single Action Controllers” which will be perfect in this scenario.

class ProfileImageController extends Controller {
public function __invoke() { /*...*/ }
}

class MessageController extends Controller {
public function __invoke() { /*...*/ }
}

class UserController extends Controller {
public function create() { /*...*/ }
public function store() { /*...*/ }
}

Now, each controller is like a specialist doctor rather than a one-size-fits-all GP.

Models Doing Too Much:

Violation: This model handles order placement, payment validation, shipping, and email updates. Maybe it also makes coffee on the side?

class Order extends Model {
public function create() { /*...*/ }
public function validatePayment() { /*...*/ }
public function ship() { /*...*/ }
public function sendUpdateEmail() { /*...*/ }
}

Resolution: Move the methods to dedicated services or use traits to separate concerns.

class Order extends Model {
public function create() { /*...*/ }
}

class PaymentService {
public function validate(Order $order) { /*...*/ }
}

class ShippingService {
public function ship(Order $order) { /*...*/ }
}

class EmailService {
public function sendUpdate(Order $order) { /*...*/ }
}

Now, our model sticks to database operations and doesn’t dabble in areas it shouldn’t, like that coder who thinks he's a stand-up comedian at team meetings.

Utility Classes with Random Helper Functions:

Violation: This utility class does a bit of everything.

class Utils {
public function formatCurrency($amount) { /*...*/ }
public function sanitizeString($input) { /*...*/ }
public function calculateDistance($pointA, $pointB) { /*...*/ }
}

Resolution: Split into specialised classes or helper functions based on categories. Now, each utility class has one job, like a developer on a coffee break — sipping coffee and silently judging the codebase.

class CurrencyHelper {
public function format($amount) { /*...*/ }
}

class StringHelper {
public function sanitize($input) { /*...*/ }
}

class DistanceCalculator {
public function calculate($pointA, $pointB) { /*...*/ }
}

Wrapping It Up:

Adhering to the Single Responsibility Principle in Laravel (or any framework, for that matter) is certainly something to be taken seriously. It improves maintainability, reusability, and testability, making your life as a developer much easier.

The Single Responsibility Principle is like having a knife that’s only meant to spread butter — efficient, clean, and no unnecessary mess. When applied correctly, especially in a Laravel application, SRP can elevate your code quality and make your life as a developer a whole lot easier.

So, the next time you see a class or method trying to juggle multiple tasks, remember: in coding, just like in stand-up comedy, timing and delivery are everything. One thing at a time!

Happy coding, and may your classes always be single-minded!

--

--