Appearance
๐๏ธ 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 โ
| Principle | Benefit |
|---|---|
| SRP | Easier to understand, test, and maintain |
| OCP | Add features without breaking existing code |
| LSP | Reliable polymorphism and substitution |
| ISP | Cleaner, more focused interfaces |
| DIP | Flexible, 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.