×

Last Updated: April 29, 2025

Mastering SOLID Principles in Laravel: A Complete Guide with Code Examples


solid-principles-in-laravel-explained-with-code-examples

When you first start writing Laravel applications, you might just focus on "making it work." But if you want to build apps that are scalable, easy to maintain, and professional, you must learn to write **clean, organized code**. That's where the SOLID principles come in.

In this article, we'll dive deep into each SOLID principle with real-world Laravel examples, explained in a way that's simple, practical, and human!

What are SOLID Principles?

SOLID is an acronym that stands for:

Let's go through them one by one — and see how they fit beautifully in your Laravel projects!


Single Responsibility Principle (SRP)

Definition: A class should have one and only one reason to change.

In Laravel, it's common to put too much logic inside controllers. Let's see a bad example first:

// Bad Example - Too much responsibility in Controller
class UserController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required',
            'email' => 'required|email'
        ]);

        $user = new User();
        $user->name = $validated['name'];
        $user->email = $validated['email'];
        $user->save();

        // Also sending email here!
        Mail::to($user->email)->send(new WelcomeEmail($user));

        return redirect()->route('home');
    }
}

What's wrong? The controller is handling validation, database saving, and sending email. Three responsibilities!

Good Example using SRP:

// Create a Service Class
class UserService
{
    public function createUser(array $data): User
    {
        $user = User::create($data);
        Mail::to($user->email)->send(new WelcomeEmail($user));
        return $user;
    }
}

// Then Controller becomes clean
class UserController extends Controller
{
    protected $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required',
            'email' => 'required|email'
        ]);

        $this->userService->createUser($validated);

        return redirect()->route('home');
    }
}

Now each class does one thing only!


Open/Closed Principle (OCP)

Definition: Software entities should be open for extension, but closed for modification.

Imagine you have a notification system in Laravel:

// Initial Notification Sender
class NotificationSender
{
    public function send($user, $message)
    {
        Mail::to($user->email)->send(new NotificationEmail($message));
    }
}

Now you want to add SMS notifications. You could modify NotificationSender, but that would break OCP. Instead:

// Create a Notifier Interface
interface Notifier
{
    public function send($user, $message);
}

// Mail Notifier
class EmailNotifier implements Notifier
{
    public function send($user, $message)
    {
        Mail::to($user->email)->send(new NotificationEmail($message));
    }
}

// SMS Notifier
class SMSNotifier implements Notifier
{
    public function send($user, $message)
    {
        // Assume Twilio integration
        SMS::send($user->phone, $message);
    }
}

// Use them flexibly
class NotificationService
{
    protected $notifier;

    public function __construct(Notifier $notifier)
    {
        $this->notifier = $notifier;
    }

    public function notify($user, $message)
    {
        $this->notifier->send($user, $message);
    }
}

Now you can add new notification types without touching existing code!


Liskov Substitution Principle (LSP)

Definition: Subclasses must be substitutable for their base classes.

This means if you write a method that expects a certain class, it should work if you pass a subclass too.

Bad LSP Violation Example:

class Bird
{
    public function fly()
    {
        // Fly in the sky
    }
}

class Ostrich extends Bird
{
    public function fly()
    {
        throw new Exception('Ostrich cannot fly!');
    }
}

Ostrich can't actually fly, so it violates expectations!

Better Approach in Laravel Context:

// Correct hierarchy
interface Bird
{
    public function move();
}

class FlyingBird implements Bird
{
    public function move()
    {
        // flying logic
    }
}

class WalkingBird implements Bird
{
    public function move()
    {
        // walking logic
    }
}

When you respect LSP, you don't get surprise behavior in Laravel apps, especially when working with polymorphism (e.g., Strategy Patterns, Service Containers).


Interface Segregation Principle (ISP)

Definition: No client should be forced to depend on methods it does not use.

Let's imagine you have a very fat interface:

// Bad interface
interface Worker
{
    public function work();
    public function eat();
}

class HumanWorker implements Worker
{
    public function work() { /* working */ }
    public function eat() { /* eating */ }
}

class RobotWorker implements Worker
{
    public function work() { /* working */ }
    public function eat() { throw new Exception('Robots do not eat!'); }
}

Problem: Robot doesn't need an eat() method.

Better Solution: Split interfaces:

interface Workable
{
    public function work();
}

interface Eatable
{
    public function eat();
}

class HumanWorker implements Workable, Eatable
{
    public function work() { /* working */ }
    public function eat() { /* eating */ }
}

class RobotWorker implements Workable
{
    public function work() { /* working */ }
}

In Laravel, this helps when creating repository interfaces, service contracts, etc., keeping them focused and clean.


Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

In Laravel, this is super easy thanks to dependency injection!

Bad Example:

class OrderService
{
    protected $paymentGateway;

    public function __construct()
    {
        $this->paymentGateway = new StripePaymentGateway();
    }

    public function pay($amount)
    {
        $this->paymentGateway->charge($amount);
    }
}

Better Approach: Depend on abstraction!

// PaymentGateway Interface
interface PaymentGateway
{
    public function charge($amount);
}

// Stripe Implementation
class StripePaymentGateway implements PaymentGateway
{
    public function charge($amount)
    {
        // Stripe logic here
    }
}

// Order Service depends on abstraction
class OrderService
{
    protected $paymentGateway;

    public function __construct(PaymentGateway $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }

    public function pay($amount)
    {
        $this->paymentGateway->charge($amount);
    }
}

Now you can easily swap out StripePaymentGateway with a PaypalPaymentGateway or even a mock in tests — without touching the OrderService itself!


Conclusion

Applying the SOLID principles in Laravel isn't just for "big" applications — it's about writing better code today. Your future self (and your team) will thank you when your code is easy to read, change, and extend.

Start small. Apply one principle at a time. Maybe refactor your next feature to follow the Single Responsibility Principle. Gradually, Laravel apps become beautiful, powerful, and professional!

Happy coding!