SOLID Principles in Laravel
Understanding SOLID Principles
SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin and are fundamental to object-oriented design.
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
1. Single Responsibility Principle (SRP)
A class should have only one reason to change. In other words, a class should have only one job or responsibility.
// ❌ BAD: Violates SRP - Multiple Responsibilities
class UserManager
{
public function register(array $data)
{
// Validate data
$validator = Validator::make($data, [
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
if ($validator->fails()) {
throw new ValidationException($validator);
}
// Create user
$user = User::create([
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
// Send email
Mail::to($user->email)->send(new WelcomeEmail($user));
// Log activity
Log::info('User registered: ' . $user->email);
// Update statistics
Cache::increment('total_users');
return $user;
}
}
// ✅ GOOD: Follows SRP - Single Responsibility per Class
class UserRepository
{
public function create(array $data): User
{
return User::create([
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}
class UserNotificationService
{
public function sendWelcomeEmail(User $user): void
{
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}
class UserActivityLogger
{
public function logRegistration(User $user): void
{
Log::info('User registered: ' . $user->email);
}
}
class UserStatisticsService
{
public function incrementUserCount(): void
{
Cache::increment('total_users');
}
}
// Controller coordinates these services
class RegisterController extends Controller
{
public function __construct(
private UserRepository $userRepository,
private UserNotificationService $notificationService,
private UserActivityLogger $activityLogger,
private UserStatisticsService $statisticsService
) {}
public function register(RegisterRequest $request)
{
$user = $this->userRepository->create($request->validated());
$this->notificationService->sendWelcomeEmail($user);
$this->activityLogger->logRegistration($user);
$this->statisticsService->incrementUserCount();
return response()->json(['user' => $user]);
}
}
2. 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.
// ❌ BAD: Violates OCP - Must modify class to add payment methods
class PaymentProcessor
{
public function process(string $type, float $amount)
{
if ($type === 'credit_card') {
// Process credit card
return $this->processCreditCard($amount);
} elseif ($type === 'paypal') {
// Process PayPal
return $this->processPayPal($amount);
} elseif ($type === 'stripe') {
// Process Stripe
return $this->processStripe($amount);
}
// Adding new payment method requires modifying this class!
}
}
// ✅ GOOD: Follows OCP - Use interfaces and polymorphism
interface PaymentGateway
{
public function process(float $amount): PaymentResult;
public function getName(): string;
}
class CreditCardGateway implements PaymentGateway
{
public function process(float $amount): PaymentResult
{
// Credit card processing logic
return new PaymentResult(true, 'CC-' . uniqid());
}
public function getName(): string
{
return 'credit_card';
}
}
class PayPalGateway implements PaymentGateway
{
public function process(float $amount): PaymentResult
{
// PayPal processing logic
return new PaymentResult(true, 'PP-' . uniqid());
}
public function getName(): string
{
return 'paypal';
}
}
class StripeGateway implements PaymentGateway
{
public function process(float $amount): PaymentResult
{
// Stripe processing logic
return new PaymentResult(true, 'ST-' . uniqid());
}
public function getName(): string
{
return 'stripe';
}
}
// Payment processor doesn't need modification for new gateways
class PaymentProcessor
{
private array $gateways = [];
public function registerGateway(PaymentGateway $gateway): void
{
$this->gateways[$gateway->getName()] = $gateway;
}
public function process(string $gatewayName, float $amount): PaymentResult
{
if (!isset($this->gateways[$gatewayName])) {
throw new \InvalidArgumentException("Gateway not found: {$gatewayName}");
}
return $this->gateways[$gatewayName]->process($amount);
}
}
// Service Provider registration
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PaymentProcessor::class, function ($app) {
$processor = new PaymentProcessor();
$processor->registerGateway(new CreditCardGateway());
$processor->registerGateway(new PayPalGateway());
$processor->registerGateway(new StripeGateway());
// Add new gateway here without modifying PaymentProcessor
return $processor;
});
}
}
3. 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.
// ❌ BAD: Violates LSP - Square changes Rectangle behavior
class Rectangle
{
protected float $width;
protected float $height;
public function setWidth(float $width): void
{
$this->width = $width;
}
public function setHeight(float $height): void
{
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
}
class Square extends Rectangle
{
public function setWidth(float $width): void
{
$this->width = $width;
$this->height = $width; // Breaks LSP!
}
public function setHeight(float $height): void
{
$this->width = $height;
$this->height = $height; // Breaks LSP!
}
}
// This breaks when using Square
function testRectangle(Rectangle $rectangle)
{
$rectangle->setWidth(5);
$rectangle->setHeight(10);
// Expected: 50, but Square gives: 100
return $rectangle->getArea();
}
// ✅ GOOD: Follows LSP - Use composition instead of inheritance
interface Shape
{
public function getArea(): float;
}
class Rectangle implements Shape
{
public function __construct(
private float $width,
private float $height
) {}
public function getArea(): float
{
return $this->width * $this->height;
}
}
class Square implements Shape
{
public function __construct(private float $side) {}
public function getArea(): float
{
return $this->side * $this->side;
}
}
// Both can be used interchangeably
function calculateArea(Shape $shape): float
{
return $shape->getArea();
}
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use. Split large interfaces into smaller, more specific ones.
// ❌ BAD: Violates ISP - Fat interface
interface Worker
{
public function work(): void;
public function eat(): void;
public function sleep(): void;
public function getPaid(): void;
}
class HumanWorker implements Worker
{
public function work(): void { /* works */ }
public function eat(): void { /* eats */ }
public function sleep(): void { /* sleeps */ }
public function getPaid(): void { /* gets paid */ }
}
class RobotWorker implements Worker
{
public function work(): void { /* works */ }
public function eat(): void { /* robots don't eat! */ }
public function sleep(): void { /* robots don't sleep! */ }
public function getPaid(): void { /* robots don't get paid! */ }
}
// ✅ GOOD: Follows ISP - Segregated interfaces
interface Workable
{
public function work(): void;
}
interface Eatable
{
public function eat(): void;
}
interface Sleepable
{
public function sleep(): void;
}
interface Payable
{
public function getPaid(): void;
}
class HumanWorker implements Workable, Eatable, Sleepable, Payable
{
public function work(): void { /* works */ }
public function eat(): void { /* eats */ }
public function sleep(): void { /* sleeps */ }
public function getPaid(): void { /* gets paid */ }
}
class RobotWorker implements Workable
{
public function work(): void { /* works */ }
// Only implements what it needs!
}
// Laravel Example
interface Authenticatable
{
public function getAuthIdentifier();
public function getAuthPassword();
}
interface CanResetPassword
{
public function getEmailForPasswordReset();
public function sendPasswordResetNotification($token);
}
interface MustVerifyEmail
{
public function hasVerifiedEmail();
public function markEmailAsVerified();
public function sendEmailVerificationNotification();
}
// User implements only needed interfaces
class User extends Model implements Authenticatable, CanResetPassword
{
// Only implements authentication and password reset
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Laravel's service container makes this easy.
- Depend on abstractions (interfaces), not concrete implementations
- Use dependency injection to provide implementations
- Makes code more testable and flexible
// ❌ BAD: Violates DIP - Depends on concrete class
class OrderProcessor
{
private MySqlOrderRepository $repository;
private SmtpEmailService $emailService;
public function __construct()
{
// Tight coupling to concrete implementations
$this->repository = new MySqlOrderRepository();
$this->emailService = new SmtpEmailService();
}
public function process(array $orderData)
{
$order = $this->repository->create($orderData);
$this->emailService->send($order->customer_email, 'Order Placed');
return $order;
}
}
// ✅ GOOD: Follows DIP - Depends on abstractions
interface OrderRepositoryInterface
{
public function create(array $data): Order;
public function find(int $id): ?Order;
}
interface EmailServiceInterface
{
public function send(string $to, string $subject, array $data = []): void;
}
class MySqlOrderRepository implements OrderRepositoryInterface
{
public function create(array $data): Order
{
return Order::create($data);
}
public function find(int $id): ?Order
{
return Order::find($id);
}
}
class MongoOrderRepository implements OrderRepositoryInterface
{
public function create(array $data): Order
{
// MongoDB implementation
}
public function find(int $id): ?Order
{
// MongoDB implementation
}
}
class SmtpEmailService implements EmailServiceInterface
{
public function send(string $to, string $subject, array $data = []): void
{
Mail::to($to)->send(new OrderPlacedMail($data));
}
}
class SendGridEmailService implements EmailServiceInterface
{
public function send(string $to, string $subject, array $data = []): void
{
// SendGrid implementation
}
}
// OrderProcessor depends on abstractions
class OrderProcessor
{
public function __construct(
private OrderRepositoryInterface $repository,
private EmailServiceInterface $emailService
) {}
public function process(array $orderData)
{
$order = $this->repository->create($orderData);
$this->emailService->send(
$order->customer_email,
'Order Placed',
['order' => $order]
);
return $order;
}
}
// Service Provider bindings
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
OrderRepositoryInterface::class,
MySqlOrderRepository::class
);
$this->app->bind(
EmailServiceInterface::class,
SmtpEmailService::class
);
}
}
// Testing is now easy - inject mocks
class OrderProcessorTest extends TestCase
{
public function test_processes_order()
{
$mockRepo = Mockery::mock(OrderRepositoryInterface::class);
$mockEmail = Mockery::mock(EmailServiceInterface::class);
$mockRepo->shouldReceive('create')->once()->andReturn(new Order());
$mockEmail->shouldReceive('send')->once();
$processor = new OrderProcessor($mockRepo, $mockEmail);
$processor->process(['customer_id' => 1]);
}
}
class BlogPostManager {
public function publish($data) {
// Validates data
// Creates post
// Sends notifications
// Updates SEO sitemap
// Clears cache
}
}
Split into: BlogPostRepository, NotificationService, SeoService, CacheService
- Email notifications
- SMS notifications
- Push notifications
- Should be easy to add Slack/Discord without modifying existing code
- Create FileStorageInterface with upload/download/delete methods
- Implement LocalFileStorage and S3FileStorage
- Create a FileManager that depends on the interface
- Register bindings in service provider