Mastering the Liskov Substitution 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.
Interface Segregation Principle in Laravel Have you ever been to a restaurant where the menu is so huge that it takes forever to find what you want? Or used a TV remote with 50 buttons when you only need 5? That's exactly what the Interface Segregati...
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 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 avoid in programming! Let's dive into this fascinating principle and see how it applies in Laravel applications.
The Liskov Substitution Principle, introduced by Barbara Liskov in 1987, states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In simpler terms, if you're using a parent class, you should be able to swap it with any of its child classes without unexpected behavior.
Let's build a payment processing system for an e-commerce platform using Laravel. We'll start with a basic implementation and then improve it using LSP.
// app/Services/Payment/PaymentProcessor.php
abstract class PaymentProcessor
{
abstract public function processPayment(float $amount): bool;
abstract public function refund(float $amount): bool;
}
class StripePaymentProcessor extends PaymentProcessor
{
public function processPayment(float $amount): bool
{
// Stripe-specific implementation
return true;
}
public function refund(float $amount): bool
{
// Stripe-specific refund implementation
return true;
}
}
class CashPaymentProcessor extends PaymentProcessor
{
public function processPayment(float $amount): bool
{
// Cash payment implementation
return true;
}
public function refund(float $amount): bool
{
// This violates LSP because cash payments typically can't be
// automatically refunded like digital payments
throw new \Exception('Cash payments cannot be refunded automatically');
}
}
The above implementation violates LSP because the CashPaymentProcessor doesn't fully substitute the PaymentProcessor - it throws an exception for refunds instead of handling them consistently.
// app/Services/Payment/PaymentProcessor.php
interface PaymentProcessorInterface
{
public function processPayment(float $amount): bool;
}
interface RefundablePaymentInterface
{
public function refund(float $amount): bool;
}
class StripePaymentProcessor implements PaymentProcessorInterface, RefundablePaymentInterface
{
public function processPayment(float $amount): bool
{
// Implementation for processing Stripe payment
return true;
}
public function refund(float $amount): bool
{
// Implementation for Stripe refund
return true;
}
}
class CashPaymentProcessor implements PaymentProcessorInterface
{
public function processPayment(float $amount): bool
{
// Implementation for cash payment
return true;
}
}
// app/Http/Controllers/PaymentController.php
class PaymentController extends Controller
{
public function processPayment(PaymentProcessorInterface $processor, float $amount)
{
try {
$success = $processor->processPayment($amount);
if ($success) {
return response()->json(['message' => 'Payment processed successfully']);
}
return response()->json(['message' => 'Payment failed'], 400);
} catch (\Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function processRefund(RefundablePaymentInterface $processor, float $amount)
{
try {
$success = $processor->refund($amount);
if ($success) {
return response()->json(['message' => 'Refund processed successfully']);
}
return response()->json(['message' => 'Refund failed'], 400);
} catch (\Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);
}
}
}
// app/Providers/PaymentServiceProvider.php
class PaymentServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PaymentProcessorInterface::class, function ($app) {
// You can switch between payment processors based on configuration
return new StripePaymentProcessor();
});
}
}
Clear Separation of Concerns: By separating refundable and non-refundable payment methods, we maintain a clear distinction in functionality.
Type Safety: Laravel's dependency injection container can safely inject the correct payment processor based on the interface.
Easy Extension: Adding new payment methods (like PayPal or Bitcoin) becomes straightforward as they only need to implement the relevant interfaces.
Better Error Handling: No unexpected exceptions from unsupported operations.
Think of payment processors like different types of vehicles:
A car and a bicycle are both vehicles (they implement PaymentProcessorInterface)
But only the car can go in reverse (implements RefundablePaymentInterface)
You wouldn't expect a bicycle to go in reverse, so it doesn't implement that interface
Don't Force Functionality: If a subclass can't reasonably implement a method from the parent class, it's probably violating LSP.
Avoid Type Checking: Instead of checking payment processor types, rely on interfaces:
// โ Bad
if ($processor instanceof StripePaymentProcessor) {
// do something
}
// โ
Good
if ($processor instanceof RefundablePaymentInterface) {
// do something
}
$this->app->bind(PaymentProcessorInterface::class, function ($app) {
return config('payments.default') === 'stripe'
? new StripePaymentProcessor()
: new CashPaymentProcessor();
});
public function __construct(private PaymentProcessorInterface $paymentProcessor) {}
interface PaymentProcessorInterface extends
ProcessPaymentInterface,
LoggableInterface,
ValidatableInterface
{}
The Liskov Substitution Principle is more than just a theoretical concept - it's a practical guide for writing maintainable, extensible code in Laravel applications. By following LSP, you create more robust systems that can easily adapt to changing requirements while maintaining stability and predictability.
Remember: If it looks like a duck, swims like a duck, and quacks like a duck, but needs batteries โ you probably have the wrong abstraction!
Happy coding! ๐