If you’ve been writing Laravel applications for a while, you’ve probably run into code that’s painful to change — where fixing one bug breaks three other things, or adding a small feature means rewriting half a class. This usually happens when SOLID principles aren’t being followed. In this guide to SOLID principles in Laravel, we’ll explain what SOLID means and walk through practical, Laravel-specific examples of each principle — written so both beginners and senior developers can get real value from it. For more Laravel and PHP content, check out our Coding category.
What Are the SOLID Principles in Laravel?
SOLID is an acronym for five object-oriented design principles that help you write code that’s easier to maintain, test, and extend:
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
These aren’t Laravel-specific rules — they apply to object-oriented programming in general — but Laravel’s structure (Controllers, Models, Services, Repositories) makes them especially relevant to how you organize your application.
S — Single Responsibility Principle
A class should have only one reason to change — meaning it should do one thing, and do it well.
❌ Violates SRP
class UserController extends Controller
{
public function register(Request $request)
{
// Validation
$request->validate([
'email' => 'required|email',
'password' => 'required|min:8',
]);
// Business logic
$user = User::create([
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Sending email
Mail::to($user->email)->send(new WelcomeEmail($user));
// Logging
Log::info('New user registered: ' . $user->email);
return response()->json($user);
}
}
This controller method is handling validation, user creation, email sending, and logging — four separate responsibilities crammed into one method.
✅ Follows SRP
class UserController extends Controller
{
public function __construct(private UserRegistrationService $registrationService) {}
public function register(RegisterUserRequest $request)
{
$user = $this->registrationService->register($request->validated());
return response()->json($user);
}
}
class UserRegistrationService
{
public function register(array $data): User
{
$user = User::create([
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
Mail::to($user->email)->send(new WelcomeEmail($user));
Log::info('New user registered: ' . $user->email);
return $user;
}
}
Now validation lives in a Form Request, and registration logic lives in a dedicated Service class. The controller’s only job is to receive the request and return a response — each piece can now be changed independently.
O — Open/Closed Principle
Classes should be open for extension, but closed for modification — meaning you should be able to add new behavior without changing existing, tested code.
❌ Violates OCP
class PaymentService
{
public function pay(string $method, float $amount)
{
if ($method === 'stripe') {
// Stripe payment logic
} elseif ($method === 'paypal') {
// PayPal payment logic
} elseif ($method === 'jazzcash') {
// JazzCash payment logic
}
// Every new payment method means editing this class again
}
}
✅ Follows OCP
interface PaymentGateway
{
public function pay(float $amount): bool;
}
class StripeGateway implements PaymentGateway
{
public function pay(float $amount): bool
{
// Stripe-specific logic
return true;
}
}
class JazzCashGateway implements PaymentGateway
{
public function pay(float $amount): bool
{
// JazzCash-specific logic
return true;
}
}
class PaymentService
{
public function __construct(private PaymentGateway $gateway) {}
public function pay(float $amount): bool
{
return $this->gateway->pay($amount);
}
}
Now adding a new payment method (like Easypaisa) just means creating a new class that implements PaymentGateway — the existing PaymentService class never needs to be touched or re-tested.
L — Liskov Substitution Principle
Subclasses should be replaceable with their parent class without breaking the application. If a child class changes expected behavior in a way that surprises the caller, it violates this principle.
❌ Violates LSP
class Bird
{
public function fly()
{
return "Flying...";
}
}
class Penguin extends Bird
{
public function fly()
{
throw new Exception("Penguins can't fly!");
}
}
Any code that expects a Bird and calls fly() will break unexpectedly when a Penguin is passed in — even though a Penguin technically “is a” Bird.
✅ Follows LSP (Laravel Example)
interface Notifiable
{
public function send(string $message): void;
}
class EmailNotification implements Notifiable
{
public function send(string $message): void
{
// send via email
}
}
class SmsNotification implements Notifiable
{
public function send(string $message): void
{
// send via SMS
}
}
// Any Notifiable can be used interchangeably without breaking anything
function notifyUser(Notifiable $notifier, string $message)
{
$notifier->send($message);
}
Both EmailNotification and SmsNotification can be swapped in without changing how notifyUser() behaves — that’s LSP done correctly.

I — Interface Segregation Principle
Don’t force a class to implement methods it doesn’t need. Instead of one large interface, split it into smaller, more specific ones.
❌ Violates ISP
interface OrderProcessor
{
public function processOnlinePayment();
public function processCashOnDelivery();
public function generateInvoice();
public function shipOrder();
}
class CashOnDeliveryOrder implements OrderProcessor
{
public function processOnlinePayment()
{
throw new Exception("Not applicable for COD");
}
// Forced to implement a method it will never use
}
✅ Follows ISP
interface Payable
{
public function processPayment();
}
interface Invoiceable
{
public function generateInvoice();
}
interface Shippable
{
public function shipOrder();
}
class OnlineOrder implements Payable, Invoiceable, Shippable
{
public function processPayment() { /* ... */ }
public function generateInvoice() { /* ... */ }
public function shipOrder() { /* ... */ }
}
class CashOnDeliveryOrder implements Invoiceable, Shippable
{
public function generateInvoice() { /* ... */ }
public function shipOrder() { /* ... */ }
}
Now each class only implements the interfaces it actually needs — no forced, meaningless method implementations.
D — Dependency Inversion Principle
High-level classes shouldn’t depend directly on low-level classes — both should depend on abstractions (interfaces). This is where Laravel’s Service Container shines.
❌ Violates DIP
class ReportService
{
public function generate()
{
$mailer = new SmtpMailer(); // directly tied to one specific implementation
$mailer->send('Report generated');
}
}
✅ Follows DIP (Laravel Example)
interface Mailer
{
public function send(string $message): void;
}
class SmtpMailer implements Mailer
{
public function send(string $message): void
{
// send via SMTP
}
}
class ReportService
{
public function __construct(private Mailer $mailer) {}
public function generate()
{
$this->mailer->send('Report generated');
}
}
// In a Laravel Service Provider:
$this->app->bind(Mailer::class, SmtpMailer::class);
Now ReportService depends on the Mailer interface, not a specific implementation. You can swap SmtpMailer for a QueuedMailer or a test mock without touching ReportService at all — this is exactly how Laravel’s dependency injection and service container are designed to be used. You can read more about this in Laravel’s own Service Container documentation.
Why SOLID Matters More As Your Laravel App Grows
For a small project or a quick prototype, ignoring the SOLID principles in Laravel won’t hurt much. But as your codebase grows — more controllers, more models, more business logic — code that ignores these principles becomes exponentially harder to test, debug, and extend. Senior developers aren’t distinguished by knowing more syntax; they’re distinguished by writing code that’s still easy to work with a year later.
Final Thoughts
The SOLID principles in Laravel aren’t abstract theory — they directly translate into Laravel’s own architecture patterns: Form Requests (SRP), interfaces and the Service Container (DIP, OCP), and well-designed contracts (ISP, LSP). Start applying even one or two of these SOLID principles in your next Laravel project, and you’ll notice your code becomes significantly easier to maintain and test. Have questions about implementing this in your own project? Reach out through our Contact page.
Frequently Asked Questions
Do I need to apply all 5 SOLID principles in every Laravel project?
Not necessarily for small projects or prototypes. However, for any application expected to grow or be maintained long-term, applying SOLID principles — especially SRP and DIP — pays off significantly as complexity increases.
Does Laravel enforce SOLID principles automatically?
No, Laravel doesn’t enforce SOLID principles, but its architecture (Service Container, interfaces, Form Requests, Service Providers) is designed to make following SOLID principles natural and convenient.
Is SOLID only relevant for large teams?
No — even solo developers benefit from SOLID principles, especially when returning to their own code months later, or when a small project unexpectedly grows into a larger one.

