Skip to content

๐Ÿ›๏ธ SOLID Principles in Laravel โ€‹

Master the five fundamental principles of object-oriented design in the context of Laravel applications. These principles lead to more maintainable, flexible, and testable code.

๐ŸŽฏ S - Single Responsibility Principle (SRP) โ€‹

"A class should have one, and only one, reason to change."

Each class should focus on a single task or responsibility. This makes classes easier to understand, test, and maintain.

โœ… Good Example โ€‹

php
<?php

declare(strict_types=1);

namespace App\Services\Orders;

use App\Models\Order;
use Illuminate\Support\Facades\Log;

/**
 * Responsible ONLY for order calculations
 */
class OrderCalculator
{
    public function calculateSubtotal(Order $order): int
    {
        return $order->items()->sum(function ($item) {
            return $item->price * $item->quantity;
        });
    }

    public function calculateDiscount(Order $order): int
    {
        if ($order->user->is_premium) {
            return (int) round($this->calculateSubtotal($order) * 0.10);
        }

        if ($order->coupon_code) {
            return $this->applyCoupon($order);
        }

        return 0;
    }

    public function calculateTax(Order $order): int
    {
        $taxableAmount = $this->calculateSubtotal($order) - $this->calculateDiscount($order);
        return (int) round($taxableAmount * 0.15);
    }

    public function calculateTotal(Order $order): int
    {
        $subtotal = $this->calculateSubtotal($order);
        $discount = $this->calculateDiscount($order);
        $tax = $this->calculateTax($order);

        return $subtotal - $discount + $tax;
    }

    private function applyCoupon(Order $order): int
    {
        // Coupon logic
        return 500; // cents
    }
}

/**
 * Responsible ONLY for order notifications
 */
class OrderNotifier
{
    public function sendConfirmation(Order $order): void
    {
        $order->user->notify(new \App\Notifications\OrderConfirmed($order));
        
        Log::info('Order confirmation sent', [
            'order_id' => $order->id,
            'user_id' => $order->user_id,
        ]);
    }

    public function sendShippingUpdate(Order $order, string $trackingNumber): void
    {
        $order->user->notify(new \App\Notifications\OrderShipped($order, $trackingNumber));
    }
}

/**
 * Responsible ONLY for order validation
 */
class OrderValidator
{
    public function validate(Order $order): bool
    {
        return $this->hasItems($order) 
            && $this->hasValidItems($order)
            && $this->hasValidAddress($order);
    }

    private function hasItems(Order $order): bool
    {
        return $order->items()->count() > 0;
    }

    private function hasValidItems(Order $order): bool
    {
        return $order->items()->every(fn ($item) => $item->quantity > 0);
    }

    private function hasValidAddress(Order $order): bool
    {
        return !empty($order->shipping_address);
    }
}

โŒ Bad Example โ€‹

php
<?php

// God class - handles too many responsibilities
class OrderManager
{
    public function process(Order $order): void
    {
        // Calculation responsibility
        $total = $order->items()->sum('price');
        $discount = $order->user->is_premium ? 1000 : 0;
        $tax = ($total - $discount) * 0.15;
        $order->total = $total - $discount + $tax;
        
        // Validation responsibility
        if ($order->items()->count() === 0) {
            throw new \Exception('No items');
        }
        
        // Payment responsibility
        PaymentGateway::charge($order->user, $order->total);
        
        // Notification responsibility
        Mail::to($order->user)->send(new OrderConfirmation($order));
        
        // Logging responsibility
        Log::info('Order processed', ['order_id' => $order->id]);
        
        // Analytics responsibility
        Analytics::track('order_completed', $order);
    }
}

๐Ÿ”“ O - Open/Closed Principle (OCP) โ€‹

"Software entities should be open for extension, but closed for modification."

You should be able to add new functionality without changing existing code. Use interfaces and abstractions to achieve this.

โœ… Good Example โ€‹

php
<?php

declare(strict_types=1);

namespace App\Services\Payments;

use App\Models\Payment;

/**
 * Payment gateway interface - open for extension
 */
interface PaymentGatewayInterface
{
    public function charge(int $amount, array $metadata = []): string;
    public function refund(string $transactionId, int $amount): bool;
    public function getStatus(string $transactionId): string;
}

/**
 * Stripe implementation - extends without modifying interface
 */
class StripeGateway implements PaymentGatewayInterface
{
    public function __construct(
        private string $apiKey
    ) {}

    public function charge(int $amount, array $metadata = []): string
    {
        // Stripe-specific implementation
        $stripe = new \Stripe\StripeClient($this->apiKey);
        $charge = $stripe->charges->create([
            'amount' => $amount,
            'currency' => 'usd',
            'metadata' => $metadata,
        ]);

        return $charge->id;
    }

    public function refund(string $transactionId, int $amount): bool
    {
        $stripe = new \Stripe\StripeClient($this->apiKey);
        $refund = $stripe->refunds->create([
            'charge' => $transactionId,
            'amount' => $amount,
        ]);

        return $refund->status === 'succeeded';
    }

    public function getStatus(string $transactionId): string
    {
        $stripe = new \Stripe\StripeClient($this->apiKey);
        $charge = $stripe->charges->retrieve($transactionId);

        return $charge->status;
    }
}

/**
 * PayPal implementation - extends without modifying interface
 */
class PayPalGateway implements PaymentGatewayInterface
{
    public function __construct(
        private string $clientId,
        private string $clientSecret
    ) {}

    public function charge(int $amount, array $metadata = []): string
    {
        // PayPal-specific implementation
        return 'tx_paypal_456';
    }

    public function refund(string $transactionId, int $amount): bool
    {
        // PayPal refund logic
        return true;
    }

    public function getStatus(string $transactionId): string
    {
        // PayPal status check
        return 'completed';
    }
}

/**
 * Payment service - closed for modification, open for extension
 * Can work with any PaymentGatewayInterface implementation
 */
class PaymentService
{
    public function __construct(
        private PaymentGatewayInterface $gateway
    ) {}

    public function processPayment(int $amount, array $metadata = []): Payment
    {
        $transactionId = $this->gateway->charge($amount, $metadata);

        return Payment::create([
            'amount' => $amount,
            'transaction_id' => $transactionId,
            'status' => 'completed',
            'gateway' => get_class($this->gateway),
        ]);
    }

    public function refundPayment(Payment $payment): bool
    {
        return $this->gateway->refund($payment->transaction_id, $payment->amount);
    }
}

// Service Provider - bind implementation
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(PaymentGatewayInterface::class, function () {
            $gateway = config('payment.default_gateway');

            return match ($gateway) {
                'stripe' => new StripeGateway(config('payment.stripe.key')),
                'paypal' => new PayPalGateway(
                    config('payment.paypal.client_id'),
                    config('payment.paypal.client_secret')
                ),
                default => throw new \Exception("Unsupported gateway: {$gateway}"),
            };
        });
    }
}

โŒ Bad Example โ€‹

php
<?php

// Tightly coupled - closed for extension
class PaymentService
{
    public function charge(int $amount, string $gateway): string
    {
        // Need to modify this method every time we add a new gateway
        if ($gateway === 'stripe') {
            // Stripe logic
            return 'tx_stripe_123';
        } elseif ($gateway === 'paypal') {
            // PayPal logic
            return 'tx_paypal_456';
        } elseif ($gateway === 'square') {
            // Square logic
            return 'tx_square_789';
        }

        throw new \Exception('Unsupported gateway');
    }
}

๐Ÿ”„ L - Liskov Substitution Principle (LSP) โ€‹

"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."

Derived classes must be substitutable for their base classes without altering the correctness of the program.

โœ… Good Example โ€‹

php
<?php

declare(strict_types=1);

namespace App\Services\Storage;

use Illuminate\Support\Facades\Storage;

/**
 * Storage interface that all implementations must follow
 */
interface FileStorageInterface
{
    public function store(string $path, string $contents): bool;
    public function get(string $path): ?string;
    public function delete(string $path): bool;
    public function exists(string $path): bool;
}

/**
 * Local storage implementation
 */
class LocalStorage implements FileStorageInterface
{
    public function store(string $path, string $contents): bool
    {
        return Storage::disk('local')->put($path, $contents);
    }

    public function get(string $path): ?string
    {
        return Storage::disk('local')->exists($path) 
            ? Storage::disk('local')->get($path) 
            : null;
    }

    public function delete(string $path): bool
    {
        return Storage::disk('local')->delete($path);
    }

    public function exists(string $path): bool
    {
        return Storage::disk('local')->exists($path);
    }
}

/**
 * S3 storage implementation - perfectly substitutable
 */
class S3Storage implements FileStorageInterface
{
    public function store(string $path, string $contents): bool
    {
        return Storage::disk('s3')->put($path, $contents);
    }

    public function get(string $path): ?string
    {
        return Storage::disk('s3')->exists($path) 
            ? Storage::disk('s3')->get($path) 
            : null;
    }

    public function delete(string $path): bool
    {
        return Storage::disk('s3')->delete($path);
    }

    public function exists(string $path): bool
    {
        return Storage::disk('s3')->exists($path);
    }
}

/**
 * File manager can work with any storage implementation
 */
class FileManager
{
    public function __construct(
        private FileStorageInterface $storage
    ) {}

    public function upload(string $path, string $contents): bool
    {
        // Works with LocalStorage, S3Storage, or any future implementation
        return $this->storage->store($path, $contents);
    }

    public function download(string $path): ?string
    {
        return $this->storage->get($path);
    }
}

โŒ Bad Example โ€‹

php
<?php

// Violates LSP - subclasses have different behavior
class FileStorage
{
    public function store(string $path, string $contents): bool
    {
        return Storage::put($path, $contents);
    }
}

class ReadOnlyStorage extends FileStorage
{
    public function store(string $path, string $contents): bool
    {
        // Throws exception - breaks substitutability
        throw new \Exception('Cannot write to read-only storage');
    }
}

// This will break if we substitute ReadOnlyStorage
$storage = new FileStorage(); // Works fine
$storage = new ReadOnlyStorage(); // Breaks!
$storage->store('file.txt', 'content'); // Exception!

๐Ÿ”Œ I - Interface Segregation Principle (ISP) โ€‹

"No client should be forced to depend on methods it does not use."

Create small, focused interfaces rather than large, general-purpose ones.

โœ… Good Example โ€‹

php
<?php

declare(strict_types=1);

namespace App\Contracts;

/**
 * Focused interface for email sending
 */
interface SendsEmail
{
    public function sendEmail(string $to, string $subject, string $body): bool;
}

/**
 * Focused interface for SMS sending
 */
interface SendsSms
{
    public function sendSms(string $to, string $message): bool;
}

/**
 * Focused interface for push notifications
 */
interface SendsPushNotifications
{
    public function sendPushNotification(string $deviceToken, string $title, string $body): bool;
}

/**
 * Email service implements only what it needs
 */
class EmailService implements SendsEmail
{
    public function sendEmail(string $to, string $subject, string $body): bool
    {
        // Email implementation
        return true;
    }
}

/**
 * SMS service implements only what it needs
 */
class SmsService implements SendsSms
{
    public function sendSms(string $to, string $message): bool
    {
        // SMS implementation
        return true;
    }
}

/**
 * Multi-channel service can implement multiple interfaces
 */
class NotificationService implements SendsEmail, SendsSms, SendsPushNotifications
{
    public function sendEmail(string $to, string $subject, string $body): bool
    {
        // Implementation
        return true;
    }

    public function sendSms(string $to, string $message): bool
    {
        // Implementation
        return true;
    }

    public function sendPushNotification(string $deviceToken, string $title, string $body): bool
    {
        // Implementation
        return true;
    }
}

/**
 * User notifier depends only on what it needs
 */
class UserNotifier
{
    public function __construct(
        private SendsEmail $emailService
    ) {}

    public function notifyUser(string $email, string $message): void
    {
        $this->emailService->sendEmail($email, 'Notification', $message);
    }
}

โŒ Bad Example โ€‹

php
<?php

// Fat interface - forces implementations to define unused methods
interface Messenger
{
    public function sendEmail(string $to, string $subject, string $body): bool;
    public function sendSms(string $to, string $message): bool;
    public function sendPushNotification(string $deviceToken, string $title, string $body): bool;
    public function sendSlackMessage(string $channel, string $message): bool;
    public function sendTelegram(string $chatId, string $message): bool;
}

// Email service forced to implement methods it doesn't support
class EmailService implements Messenger
{
    public function sendEmail(string $to, string $subject, string $body): bool
    {
        // Works
        return true;
    }

    // Forced to implement methods we don't support
    public function sendSms(string $to, string $message): bool
    {
        throw new \Exception('Not supported');
    }

    public function sendPushNotification(string $deviceToken, string $title, string $body): bool
    {
        throw new \Exception('Not supported');
    }

    public function sendSlackMessage(string $channel, string $message): bool
    {
        throw new \Exception('Not supported');
    }

    public function sendTelegram(string $chatId, string $message): bool
    {
        throw new \Exception('Not supported');
    }
}

๐Ÿ”€ D - Dependency Inversion Principle (DIP) โ€‹

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

Depend on interfaces/abstractions, not concrete implementations.

โœ… Good Example โ€‹

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Contracts\PaymentGatewayInterface;
use App\Contracts\EmailServiceInterface;
use App\Contracts\LoggerInterface;
use App\Models\Order;

/**
 * High-level module depends on abstractions
 */
class OrderService
{
    public function __construct(
        private PaymentGatewayInterface $paymentGateway,
        private EmailServiceInterface $emailService,
        private LoggerInterface $logger
    ) {}

    public function processOrder(Order $order): bool
    {
        try {
            // Depends on abstraction, not concrete implementation
            $this->paymentGateway->charge($order->total);
            
            $this->emailService->sendEmail(
                $order->user->email,
                'Order Confirmed',
                'Your order has been confirmed'
            );
            
            $this->logger->info('Order processed', ['order_id' => $order->id]);
            
            return true;
        } catch (\Exception $e) {
            $this->logger->error('Order processing failed', [
                'order_id' => $order->id,
                'error' => $e->getMessage(),
            ]);
            
            return false;
        }
    }
}

/**
 * Bind implementations in service provider
 */
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind abstractions to concrete implementations
        $this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);
        $this->app->bind(EmailServiceInterface::class, SendGridService::class);
        $this->app->bind(LoggerInterface::class, MonologLogger::class);
    }
}

// Easy to swap implementations
class TestServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Use different implementations for testing
        $this->app->bind(PaymentGatewayInterface::class, FakePaymentGateway::class);
        $this->app->bind(EmailServiceInterface::class, FakeEmailService::class);
        $this->app->bind(LoggerInterface::class, NullLogger::class);
    }
}

โŒ Bad Example โ€‹

php
<?php

namespace App\Services;

use App\Services\StripeGateway;
use App\Services\SendGridService;
use Illuminate\Support\Facades\Log;

/**
 * Tightly coupled to concrete implementations
 */
class OrderService
{
    private StripeGateway $paymentGateway;
    private SendGridService $emailService;

    public function __construct()
    {
        // Creates concrete dependencies directly
        $this->paymentGateway = new StripeGateway(config('stripe.key'));
        $this->emailService = new SendGridService(config('sendgrid.key'));
    }

    public function processOrder(Order $order): bool
    {
        // Tightly coupled to Stripe
        $this->paymentGateway->charge($order->total);
        
        // Tightly coupled to SendGrid
        $this->emailService->send(
            $order->user->email,
            'Order Confirmed',
            'Your order has been confirmed'
        );
        
        // Tightly coupled to Laravel Log facade
        Log::info('Order processed', ['order_id' => $order->id]);
        
        return true;
    }
}

๐Ÿ“‹ SOLID Principles Summary โ€‹

โœ… Do's โ€‹

  • Single Responsibility - One class, one job
  • Open/Closed - Use interfaces for extensions
  • Liskov Substitution - Subclasses must be truly replaceable
  • Interface Segregation - Many small interfaces over one large
  • Dependency Inversion - Depend on abstractions, not concretions

โŒ Don'ts โ€‹

  • Don't create god classes - Break them into focused classes
  • Don't modify existing code - Extend through interfaces
  • Don't break contracts - Ensure substitutability
  • Don't force unused methods - Keep interfaces focused
  • Don't couple to implementations - Always use abstractions

๐ŸŽฏ Practical Benefits โ€‹

PrincipleBenefit
SRPEasier to understand, test, and maintain
OCPAdd features without breaking existing code
LSPReliable polymorphism and substitution
ISPCleaner, more focused interfaces
DIPFlexible, testable, loosely-coupled code

๐Ÿงช Testing with SOLID โ€‹

SOLID principles make your code significantly more testable:

php
<?php

// Easy to test with dependency injection and interfaces
class OrderServiceTest extends TestCase
{
    public function test_process_order_successfully(): void
    {
        // Mock dependencies (thanks to DIP)
        $paymentGateway = Mockery::mock(PaymentGatewayInterface::class);
        $emailService = Mockery::mock(EmailServiceInterface::class);
        $logger = Mockery::mock(LoggerInterface::class);

        // Set expectations
        $paymentGateway->shouldReceive('charge')->once()->andReturn('tx_123');
        $emailService->shouldReceive('sendEmail')->once()->andReturn(true);
        $logger->shouldReceive('info')->once();

        // Create service with mocks
        $orderService = new OrderService($paymentGateway, $emailService, $logger);

        // Test
        $order = Order::factory()->make(['total' => 1000]);
        $result = $orderService->processOrder($order);

        $this->assertTrue($result);
    }
}

๐Ÿ“ SOLID in Practice: These principles work together. Apply them gradually as your application grows. Don't over-engineer small projects, but design with these principles in mind.

Built with VitePress