Mastering the Liskov Substitution Principle in Laravel: A Real-World Guide ๐Ÿš€

Sohag HasanSohag Hasan
4 min read

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 avoid in programming! Let's dive into this fascinating principle and see how it applies in Laravel applications.

What is the Liskov Substitution Principle?

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.

Real-World Example: Payment Processing System

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.

โŒ Bad Implementation (Violating 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.

โœ… Good Implementation (Following LSP)

// 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;
    }
}

Using the Payment Processors in a Controller

// 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);
        }
    }
}

Service Provider Registration

// 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();
        });
    }
}

Benefits of Following LSP in This Example

  1. Clear Separation of Concerns: By separating refundable and non-refundable payment methods, we maintain a clear distinction in functionality.

  2. Type Safety: Laravel's dependency injection container can safely inject the correct payment processor based on the interface.

  3. Easy Extension: Adding new payment methods (like PayPal or Bitcoin) becomes straightforward as they only need to implement the relevant interfaces.

  4. Better Error Handling: No unexpected exceptions from unsupported operations.

Real-World Analogies

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

Common Pitfalls to Avoid

  1. Don't Force Functionality: If a subclass can't reasonably implement a method from the parent class, it's probably violating LSP.

  2. 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
}
  1. Don't Throw Unexpected Exceptions: Subclasses should maintain the same error handling patterns as their parent classes.

Practical Tips for Laravel Applications

  1. Use Laravel's interface binding in service providers for easy swapping of implementations:
$this->app->bind(PaymentProcessorInterface::class, function ($app) {
    return config('payments.default') === 'stripe' 
        ? new StripePaymentProcessor() 
        : new CashPaymentProcessor();
});
  1. Leverage Laravel's dependency injection to automatically inject the correct implementation:
public function __construct(private PaymentProcessorInterface $paymentProcessor) {}
  1. Use interface segregation alongside LSP for more flexible code:
interface PaymentProcessorInterface extends 
    ProcessPaymentInterface,
    LoggableInterface,
    ValidatableInterface 
{}

Conclusion

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! ๐Ÿš€

0
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