Master the Open-Closed Principle in Laravel: A Real-World Guide ๐
Table of contents
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 nightmare! In this guide, we'll explore how to implement OCP in Laravel through practical, real-world examples.
What is the Open-Closed Principle?
The Open-Closed Principle states that software entities (classes, modules, functions) should be:
Open for extension: You can add new functionality
Closed for modification: Existing code shouldn't be changed
Think of it like a smartphone: you can extend its functionality by installing new apps (open for extension) without modifying the phone's core operating system (closed for modification).
Real-World Example: Notification System
Let's build a notification system for an e-commerce platform. Customers need to be notified about their order status through different channels (SMS, Email, WhatsApp).
โ Without OCP (Bad Approach)
class OrderNotifier
{
public function notify($order, $channel)
{
if ($channel === 'email') {
// Send email notification
Mail::to($order->customer_email)->send(new OrderStatusMail($order));
}
elseif ($channel === 'sms') {
// Send SMS notification
SMS::send($order->customer_phone, "Your order #{$order->id} status: {$order->status}");
}
// What if we need to add WhatsApp notification? We'd need to modify this class!
}
}
Problems with this approach:
Adding a new notification channel requires modifying existing code
The class violates Single Responsibility Principle
Testing becomes harder with each new channel
Risk of breaking existing functionality
โ With OCP (Better Approach)
First, let's create an interface:
interface NotificationChannel
{
public function send($order);
}
Now, implement specific notification channels:
class EmailNotification implements NotificationChannel
{
public function send($order)
{
Mail::to($order->customer_email)
->send(new OrderStatusMail($order));
}
}
class SMSNotification implements NotificationChannel
{
public function send($order)
{
SMS::send(
$order->customer_phone,
"Your order #{$order->id} status: {$order->status}"
);
}
}
class WhatsAppNotification implements NotificationChannel
{
public function send($order)
{
WhatsApp::sendMessage(
$order->customer_phone,
"Your order #{$order->id} status: {$order->status}"
);
}
}
Create the main notifier class:
class OrderNotifier
{
private $channel;
public function __construct(NotificationChannel $channel)
{
$this->channel = $channel;
}
public function notify($order)
{
$this->channel->send($order);
}
}
Usage in Laravel:
// In your OrderController or Service
public function updateOrderStatus(Order $order)
{
// Update order status
$order->update(['status' => 'shipped']);
// Send notification via email
$emailNotifier = new OrderNotifier(new EmailNotification());
$emailNotifier->notify($order);
// Send notification via SMS
$smsNotifier = new OrderNotifier(new SMSNotification());
$smsNotifier->notify($order);
}
Service Provider Implementation
Register your notification channels in Laravel's service container:
// app/Providers/NotificationServiceProvider.php
class NotificationServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind('notification.channels.email', function () {
return new EmailNotification();
});
$this->app->bind('notification.channels.sms', function () {
return new SMSNotification();
});
}
}
Benefits of This Approach
Easy Extension: Add new notification channels without modifying existing code
Better Testing: Test each notification channel independently
Clean Code: Each class has a single responsibility
Maintainable: Changes to one notification channel don't affect others
Dependency Injection: Laravel's IoC container handles dependencies
Real-World Practical Example: Payment Gateway Integration
Let's look at another example with payment gateways:
interface PaymentGateway
{
public function process(Order $order);
public function refund(Order $order);
}
class StripePayment implements PaymentGateway
{
public function process(Order $order)
{
// Stripe-specific implementation
return Stripe::charges()->create([
'amount' => $order->total * 100,
'currency' => 'usd',
'source' => $order->payment_token
]);
}
public function refund(Order $order)
{
// Stripe-specific refund logic
}
}
class PayPalPayment implements PaymentGateway
{
public function process(Order $order)
{
// PayPal-specific implementation
return PayPal::payment()
->setAmount($order->total)
->setOrderId($order->id)
->execute();
}
public function refund(Order $order)
{
// PayPal-specific refund logic
}
}
Potential Drawbacks and How to Address Them
Increased Initial Development Time
Solution: Consider it an investment in maintainability
The time saved in future modifications will outweigh initial setup time
More Files and Classes
Solution: Use proper namespacing and folder structure
Laravel's auto-loading makes handling multiple files efficient
Learning Curve for New Developers
Solution: Document your interfaces and implementations
Use meaningful names and maintain consistent patterns
Best Practices for OCP in Laravel
- Use Laravel's interface binding:
$this->app->bind(PaymentGateway::class, StripePayment::class);
- Leverage configuration files:
// config/services.php
return [
'payment' => [
'default' => env('PAYMENT_GATEWAY', 'stripe'),
'gateways' => [
'stripe' => StripePayment::class,
'paypal' => PayPalPayment::class,
]
]
];
- Use factory patterns when appropriate:
class PaymentGatewayFactory
{
public static function create(string $gateway): PaymentGateway
{
$gateway = config("services.payment.gateways.{$gateway}");
if (!$gateway) {
throw new InvalidArgumentException("Unsupported payment gateway");
}
return app($gateway);
}
}
Conclusion
The Open-Closed Principle might seem like extra work initially, but it's a powerful tool for creating maintainable and scalable Laravel applications. By following OCP:
Your code becomes more flexible and adaptable
You reduce the risk of breaking existing functionality
Testing becomes easier and more focused
Your application becomes more modular and professional
Remember: Good architecture is not about solving today's problems, but about making tomorrow's changes easier to implement.
Start implementing OCP in your Laravel projects today, and you'll thank yourself later when new requirements come in!
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