Introduction: Why SOLID is Not Just a Dogma, But a Tool
Most developers know that SOLID is five principles of object-oriented programming: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. However, in practice, understanding often boils down to mechanically following rules. In this article, we will go further: we will analyze advanced scenarios for applying SOLID in PHP, examine antipatterns that violate the principles, and show how refactoring with SOLID in mind makes code flexible and testable.
The material is intended for Middle+ and Senior level developers who already write in PHP and want to improve the architecture of their projects.
1. Single Responsibility Principle (SRP): When a Class "Knows Too Much"
The classic formulation: "A class should have only one reason to change." At an advanced level, it is important to understand that "reason to change" is a business requirement. If your class changes due to changes in validation rules, output format, or saving logic, this is an SRP violation.
Violation Example (God Object)
class OrderProcessor { public function process(array $data): void { // 1. Data validation if (empty($data['user_id']) || empty($data['items'])) { throw new InvalidArgumentException('Invalid order data'); } // 2. Cost calculation $total = 0; foreach ($data['items'] as $item) { $total += $item['price'] * $item['quantity']; } // 3. Saving to DB $sql = "INSERT INTO orders (user_id, total, status) VALUES (?, ?, 'pending')"; // ... executing query // 4. Sending email mail($data['user_email'], 'Order confirmation', "Total: $total"); }}Refactoring with SRP
We split into three classes with clear areas of responsibility:
class OrderValidator { public function validate(array $data): void { if (empty($data['user_id']) || empty($data['items'])) { throw new InvalidArgumentException('Invalid order data'); } }}
class OrderCalculator { public function calculateTotal(array $items): float { return array_reduce($items, function ($carry, $item) { return $carry + $item['price'] * $item['quantity']; }, 0.0); }}
class OrderRepository { public function save(int $userId, float $total, string $status = 'pending'): int { // DB saving logic return 42; // Order ID }}
class OrderProcessor { public function __construct( private OrderValidator $validator, private OrderCalculator $calculator, private OrderRepository $repository, private Mailer $mailer ) {} public function process(array $data): void { $this->validator->validate($data); $total = $this->calculator->calculateTotal($data['items']); $orderId = $this->repository->save($data['user_id'], $total); $this->mailer->send($data['user_email'], 'Order #' . $orderId); }}2. Open/Closed Principle (OCP): Extension Without Modification
The essence of OCP: classes should be open for extension but closed for modification. In practice, this means using polymorphism and abstractions instead of conditional constructs.
OCP Violation: switch-case for each new type
class PaymentProcessor { public function pay(string $type, float $amount): void { switch ($type) { case 'credit_card': // card processing break; case 'paypal': // PayPal processing break; // When adding a new type — we modify the class! } }