Master the Open-Closed Principle in Laravel: A Real-World Guide ๐

WhoAmI => notes.sohag.pro/author
Search for a command to run...

WhoAmI => notes.sohag.pro/author
No comments yet. Be the first to comment.
Liskov Substitution Principle in Laravel Have you ever tried replacing your regular coffee machine with an espresso maker, only to realize it doesn't quite fit your morning routine? That's exactly what the Liskov Substitution Principle (LSP) helps us...
I had a solid list of reasons my life wasn't moving faster, until a grainy old lecture pointed out the one name missing from it

The finale isn't a victory lap. It's the story of the control I shipped that did nothing, the footgun still sitting in my demo, and the handful of things I'd keep exactly as they are.

How do you hold a large payment for a second pair of eyes without ever letting the unapproved money touch a balance, and how do you stream that decision to the outside world without standing up a broker?

How do you show the total under a parent account when the whole system refuses to store a balance? A recursive query, a trigger that refuses to draw a circle, and a rule about what actually has to sum to zero.

Every time I wanted to change an FX rate I had to edit a file on the server and restart the app. So I moved rates and markup into a live admin API, and then audited it hard enough to find the bug that quietly undid the whole thing.

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.
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).
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).
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
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);
}
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();
});
}
}
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
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
}
}
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
$this->app->bind(PaymentGateway::class, StripePayment::class);
// config/services.php
return [
'payment' => [
'default' => env('PAYMENT_GATEWAY', 'stripe'),
'gateways' => [
'stripe' => StripePayment::class,
'paypal' => PayPalPayment::class,
]
]
];
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);
}
}
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!