SOLID Principles in PHP

SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. The five principles are: Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, and Dependency inversion principle. It is designed and developed by Robert C. Martin.

The single responsibility principle states that a class should have only one reason to change. The open/closed principle states that classes should be open for extension but closed for modification. The Liskov substitution principle states that any instance of a parent class should be replaceable by its subclasses without altering the correctness of the program. The interface segregation principle states that clients should not be forced to depend on methods they do not use. The dependency inversion principle states that high-level modules should not depend on low-level modules, but instead both should depend on abstractions. Let’s see all of this in detail

The SOLID principles help software engineers create software designs that are more understandable, flexible, and maintainable. By adhering to these principles, software engineers can create code that is easier to debug, extend, and maintain. The single responsibility principle, for example, helps engineers create code that is more modular, meaning that it can be changed without affecting the rest of the code. The open/closed principle helps engineers create code that can be easily extended without needing to modify the existing code. The Liskov substitution principle helps engineers refactor code while still ensuring that the program remains correct. The interface segregation principle helps engineers create code that is more loosely coupled, meaning that it is easier to make changes without impacting other parts of the code. Finally, the dependency inversion principle helps engineers create code that is more maintainable by ensuring that high-level modules do not depend on low-level modules.

Single Responsibility Principle

The single responsibility principle states that a class should have only one reason to change. This principle encourages code that is more modular, meaning that it can be changed without affecting the rest of the code.

For example, in PHP, a class might have methods for retrieving data from a database, formatting it for display, and displaying it to the user. This class violates the single responsibility principle, since it has three separate responsibilities (retrieving data from the database, formatting the data, and displaying it to the user).

A better approach would be to have separate classes for each responsibility. For example, one class could be responsible for retrieving data from the database, another for formatting the data, and a third for displaying the data to the user. This approach would make the code more modular, meaning that it could be more easily changed and maintained.

Here is an example of code that does not follow the single responsibility principle:

<?php

class DataHandler {
    public function getData() {
        // code to retrieve data from the database
    }
    
    public function formatData($data) {
        // code to format the data
    }
    
    public function displayData($data) {
        // code to display the data to the user
    }
}

$handler = new DataHandler();
$data = $handler->getData();
$data = $handler->formatData($data);
$handler->displayData($data);

Here is an example of code in PHP that follows the single responsibility principle:

<?php

class Database {
    public function getData() {
        // code to retrieve data from the database
    }
}

class Formatter {
    public function formatData($data) {
        // code to format the data
    }
}

class Display {
    public function displayData($data) {
        // code to display the data to the user
    }
}

$database = new Database();
$data = $database->getData();

$formatter = new Formatter();
$data = $formatter->formatData($data);

$display = new Display();
$display->displayData($data);

Open/closed Principle

The open/closed principle states that classes should be open for extension but closed for modification. This principle encourages code that is more reusable and can be easily extended without needing to modify the existing code.

For example, in PHP, a class might have a method for calculating the total of a purchase. This method could be modified to include additional discounts or taxes. However, this violates the open/closed principle, since the existing code must be changed in order to add additional discounts or taxes.

A better approach would be to have a separate class that is responsible for calculating discounts or taxes. This class can then be extended to add additional discounts or taxes without needing to change the existing code.

For example:

<?php

class Order {
    public function calculateTotal() {
        // code to calculate total
    }
}

class DiscountCalculator {
    public function calculateDiscount($total) {
        // code to calculate discount
    }
}

class TaxCalculator {
    public function calculateTax($total) {
        // code to calculate tax
    }
}

$order = new Order();
$total = $order->calculateTotal();

Here is another example of code in PHP that follows the open/closed principle:

<?php

abstract class PaymentMethod {
    abstract public function processPayment($amount);
}

class CreditCard extends PaymentMethod {
    public function processPayment($amount) {
        // code to process payment with credit card
    }
}

class PayPal extends PaymentMethod {
    public function processPayment($amount) {
        // code to process payment with PayPal
    }
}

$paymentMethod = new CreditCard();
$paymentMethod->processPayment(100);

In this example, the abstract class PaymentMethod defines an abstract method processPayment(), which must be implemented by any class that extends it. The CreditCard and PayPal classes extend PaymentMethod and implement the processPayment() method. This allows the code to be easily extended to include additional payment methods without needing to modify the existing code.

Liskov substitution principle

The Liskov substitution principle states that any instance of a parent class should be replaceable by its subclasses without altering the correctness of the program. This principle encourages code that is more modular, meaning that it can be changed without affecting the rest of the code.

For example, a parent class might define a method for calculating the area of a shape. A subclass might override this method to calculate the area of a triangle, but it should still produce the same result as the parent class method. This ensures that code that relies on the parent class can still work correctly when using the subclass.

Here is an example of code in PHP that follows the Liskov substitution principle:

<?php

abstract class Shape {
    abstract public function calculateArea();
}

class Circle extends Shape {
    private $radius;
    
    public function __construct($radius) {
        $this->radius = $radius;
    }
    
    public function calculateArea() {
        return pi() * pow($this->radius, 2);
    }
}

class Square extends Shape {
    private $side;
    
    public function __construct($side) {
        $this->side = $side;
    }
    
    public function calculateArea() {
        return pow($this->side, 2);
    }
}

function calculateTotalArea(Shape $shape) {
    return $shape->calculateArea();
}

$circle = new Circle(2);
$area = calculateTotalArea($circle); // 12.566370614359

Interface segregation principle

The interface segregation principle states that clients should not be forced to depend on methods they do not use. This principle encourages code that is more loosely coupled, meaning that it is easier to make changes without impacting other parts of the code.

For example, in PHP, a class might have several methods for retrieving data from a database. However, if a client only requires one of these methods, it should not be forced to depend on the other methods.

A better approach would be to have separate interfaces for each responsibility. For example, one interface could be responsible for retrieving data from the database, another for formatting the data, and a third for displaying the data to the user. This approach would make the code more loosely coupled, meaning that it could be more easily changed and maintained.

For example:

<?php

interface DatabaseRetriever {
    public function getData($query);
}

interface DataFormatter {
    public function formatData($data);
}

interface DataDisplayer {
    public function displayData($data);
}

class DatabaseRetrieverImpl implements DatabaseRetriever {
    public function getData($query) {
        // code to retrieve data from the database
    }
}

class FormatterImpl implements DataFormatter {
    public function formatData($data) {
        // code to format the data
    }
}

class DisplayImpl implements DataDisplayer {
    public function displayData($data) {
        // code to display the data to the user
    }
}

In this example, there are three separate interfaces: DatabaseRetriever, DataFormatter, and DataDisplayer. Each interface defines one responsibility, and any class that implements one of these interfaces is only required to implement that responsibility. This ensures that classes can be easily extended to include additional responsibilities without needing to modify the existing code. For example, if a new requirement arises to format the data before displaying it, the FormatterImpl class can be extended to include this functionality without needing to modify the DatabaseRetrieverImpl or DisplayImpl classes.

Dependency inversion principle

The dependency inversion principle states that high-level modules should not depend on low-level modules, but instead both should depend on abstractions. This principle encourages code that is more maintainable by ensuring that high-level modules do not depend on low-level modules.

For example, in PHP, a high-level module might depend on a low-level module to retrieve data from a database. This violates the dependency inversion principle, since the high-level module is now dependent on the low-level module.

A better approach would be to have an abstraction that is responsible for retrieving data from the database. This abstraction can then be implemented by a low-level module, and the high-level module can depend on the abstraction instead of the low-level module. This approach would make the code more maintainable, since the high-level module does not need to be changed if the implementation of the low-level module changes.

For example:

<?php

interface DatabaseInterface {
    public function getData($query);
}

class Database implements DatabaseInterface {
    public function getData($query) {
        // code to retrieve data from the database
    }
}

class HighLevelModule {
    private $database;
    
    public function __construct(DatabaseInterface $database) {
        $this->database = $database;
    }
    
    public function getData($query) {
        return $this->database->getData($query);
    }
}

$database = new Database();
$highLevelModule = new HighLevelModule($database);
$data = $highLevelModule->getData('SELECT * FROM table');

Here is a more realistic example of code in PHP that follows the dependency inversion principle:

<?php

interface LoggerInterface {
    public function log($message);
}

class FileLogger implements LoggerInterface {
    public function log($message) {
        // code to write message to log file
    }
}

class DatabaseLogger implements LoggerInterface {
    public function log($message) {
        // code to write message to database
    }
}

class User {
    private $logger;
    
    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    
    public function log($message) {
        $this->logger->log($message);
    }
}

In this example, the User class depends on an abstraction (the LoggerInterface) instead of a concrete class (the FileLogger or DatabaseLogger classes). This ensures that the User class does not need to be changed if the implementation of the logger classes changes. For example, if a new requirement arises to log messages to a different source, the FileLogger or DatabaseLogger classes can be replaced with a different class that implements the LoggerInterface interface, and the User class does not need to be changed.