Why SOLID Principles Matter in PHP
SOLID is an acronym for five object-oriented design principles that, when applied consistently, produce code that's easier to understand, change, and test. In a language as flexible as PHP — where you can accomplish the same task in a dozen ways — these principles serve as a compass for making good architectural decisions.
S — Single Responsibility Principle
A class should have one, and only one, reason to change.
A common violation in PHP is a "God class" that handles everything: fetching data, processing it, formatting output, and sending emails.
// Violation: one class does too much
class UserController {
public function register(array $data): void {
// validates input
// hashes password
// saves to database
// sends welcome email
// logs the event
}
}
// Better: each responsibility in its own class
class UserRegistrationService {
public function __construct(
private UserRepository $repo,
private Mailer $mailer,
private Logger $logger,
) {}
public function register(RegisterUserDTO $dto): User {
$user = $this->repo->create($dto);
$this->mailer->sendWelcome($user);
$this->logger->info('User registered', ['id' => $user->id]);
return $user;
}
}
O — Open/Closed Principle
Classes should be open for extension but closed for modification.
Instead of adding a new if branch every time requirements change, use interfaces and polymorphism:
interface NotificationChannel {
public function send(string $message, User $user): void;
}
class EmailNotification implements NotificationChannel { ... }
class SlackNotification implements NotificationChannel { ... }
class SMSNotification implements NotificationChannel { ... }
class Notifier {
public function __construct(private NotificationChannel $channel) {}
public function notify(string $msg, User $user): void {
$this->channel->send($msg, $user);
}
}
Adding a new channel requires a new class, not a change to existing code.
L — Liskov Substitution Principle
Subtypes must be substitutable for their base types without altering program correctness.
If a method accepts a Shape, passing a Circle or Rectangle should work identically. Avoid overriding methods in ways that change expected behavior — throwing exceptions where the parent returns a value, for instance, violates this principle.
I — Interface Segregation Principle
Clients should not be forced to depend on methods they do not use.
// Too broad — not all implementors need all methods
interface Repository {
public function find(int $id): Model;
public function save(Model $model): void;
public function delete(int $id): void;
public function paginate(int $page): Collection;
public function exportToCsv(): string; // Not every repo needs this!
}
// Better: split into focused interfaces
interface ReadableRepository {
public function find(int $id): Model;
public function paginate(int $page): Collection;
}
interface WritableRepository {
public function save(Model $model): void;
public function delete(int $id): void;
}
D — Dependency Inversion Principle
High-level modules should depend on abstractions, not concrete implementations.
// Bad: tightly coupled to a specific mailer
class OrderService {
private MailgunMailer $mailer;
public function __construct() {
$this->mailer = new MailgunMailer(); // hard dependency
}
}
// Good: depend on an interface, inject the implementation
class OrderService {
public function __construct(private MailerInterface $mailer) {}
}
This makes OrderService testable (you can inject a mock mailer) and flexible (swap Mailgun for SendGrid with zero changes to OrderService).
Putting It All Together
SOLID principles work best as a holistic system. Applying SRP naturally leads to more focused interfaces (ISP). Depending on abstractions (DIP) enables the OCP. Over time, these habits produce a codebase where:
- New features are added without fear of breaking existing behavior.
- Unit tests are straightforward because classes have clear boundaries.
- Code reviews are faster because intent is obvious.
- Onboarding new developers is easier because the architecture is consistent.
Start small — pick one class in your current project and ask: "Does this class have a single reason to change?" That's all it takes to begin.