SOLID principles in PHP: theory and practice
SOLID is a set of five fundamental principles of object-oriented programming formulated by Robert Martin. They help create flexible, maintainable, and scalable code. In this article, we will break down each principle using PHP examples, from simple concepts to real-world practical scenarios.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change. In other words, each class is responsible for only one task or functionality.
Violating SRP often leads to "god objects" that know and do too much. Consider a bad example:
class Report { public function generate($data) { // Generate report return "Report: " . json_encode($data); }
public function saveToFile($filename, $content) { file_put_contents($filename, $content); }
public function sendEmail($to, $subject, $body) { mail($to, $subject, $body); }}Here, the Report class is responsible for generation, saving, and sending — three different responsibilities. Let's fix this:
class ReportGenerator { public function generate($data) { return "Report: " . json_encode($data); }}
class FileSaver { public function save($filename, $content) { file_put_contents($filename, $content); }}
class EmailSender { public function send($to, $subject, $body) { mail($to, $subject, $body); }}
// Usage$report = new ReportGenerator();$content = $report->generate($data);
$saver = new FileSaver();$saver->save('report.txt', $content);
$sender = new EmailSender();$sender->send('admin@example.com', 'Report ready', $content);Practical tip: If you see the conjunction "and" in a class name (e.g., UserAndLogger), it is a sure sign of an SRP violation.
2. Open-Closed Principle (OCP)
Definition: Classes should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
Violating OCP occurs when adding a new type requires changing existing classes. Example of bad code:
class PaymentProcessor { public function process($type, $amount) { if ($type === 'credit_card') { // Process credit card } elseif ($type === 'paypal') { // Process PayPal } elseif ($type === 'crypto') { // Process cryptocurrency } // Adding a new type = modifying this class }}The solution is to use polymorphism through interfaces:
interface PaymentMethod { public function process($amount);}
class CreditCardPayment implements PaymentMethod { public function process($amount) { echo "Processing credit card for amount $amount"; }}
class PayPalPayment implements PaymentMethod { public function process($amount) { echo "Processing PayPal for amount $amount"; }}
class PaymentProcessor { public function process(PaymentMethod $method, $amount) { $method->process($amount); }}
// Usage$processor = new PaymentProcessor();$processor->process(new CreditCardPayment(), 100);$processor->process(new PayPalPayment(), 200);Now, adding a new payment method (e.g., ApplePayPayment) does not require changing PaymentProcessor.
3. Liskov Substitution Principle (LSP)
Definition: Objects of subclasses should be interchangeable with objects of the base