SOLID Principles with PHP examples

This tutorial describes and shows SOLID principles with examples in PHP.
Provided by Leumas Naypoka / www.apphp.com

Understanding SOLID principles

What is the SOLID-principles?
According to Wikipedia's definition it's abbreviation of the five basic principles of design classes in object-oriented design:

- Single responsibility
- Open-closed
- Liskov substitution
- Interface segregation
- Dependency inversion

1. Single Responsibility

So, as an example lets take popular and widely-used example - an online store with orders, products and customers. The principle states that the only responsibility - "one single duty should be imposed on each object." In other words - a specific class must solve a specific task - no more and no less. Consider the following description of the class to represent the order in the online store:

<?php
class Order
{
    public function 
calculateTotalSum(){/*...*/}
    public function 
getItems(){/*...*/}
    public function 
getItemCount(){/*...*/}
    public function 
addItem($item){/*...*/}
    public function 
deleteItem($item){/*...*/}

    public function 
printOrder(){/*...*/}
    public function 
showOrder(){/*...*/}

    public function 
load(){/*...*/}
    public function 
save(){/*...*/}
    public function 
update(){/*...*/}
    public function 
delete(){/*...*/}
}
?>

As you can see, this class performs the operation for 3 different types of tasks: work with every order (calculateTotalSum, getItems, getItemsCount, addItem, deleteItem), display order (printOrder, showOrder) and data handeling (load, save, update, delete).

What does it lead to?

This leads to the case that if we want to make changes to the print job, or storage techniques, we change the order class itself, which may lead to inoperability. To solve this problem is the division of the class into 3 classes, each of which will be to carry out their task:

<?php
class Order
{
    public function 
calculateTotalSum(){/*...*/}
    public function 
getItems(){/*...*/}
    public function 
getItemCount(){/*...*/}
    public function 
addItem($item){/*...*/}
    public function 
deleteItem($item){/*...*/}
}

class 
OrderRepository
{
    public function 
load($orderID){/*...*/}
    public function 
save($order){/*...*/}
    public function 
update($order){/*...*/}
    public function 
delete($order){/*...*/}
}

class 
OrderViewer
{
    public function 
printOrder($order){/*...*/}
    public function 
showOrder($order){/*...*/}
}
?>

Now each class is engaged in the specific task and for each class there is only one reason to change it.

2. Open-Closed Principle

This principle declares that - "software entities should be open for extension, but closed for modification." In more simple words it can be described as - all classes, functions, etc. should be designed so that to change their behavior, we do not need to modify their source code.

Consider the example of OrderRepository class.

<?php
class OrderRepository
{
    public function 
load($orderID)
    {
        
$pdo = new PDO(
            
$this->config->getDsn(),
            
$this->config->getDBUser(),
            
$this->config->getDBPassword()
        );
        
$statement $pdo->prepare("SELECT * FROM `orders` WHERE id=:id");
        
$statement->execute(array(":id" => $orderID));
        return 
$query->fetchObject("Order");
    }
    
    public function 
save($order){/*...*/}
    public function 
update($order){/*...*/}
    public function 
delete($order){/*...*/}
}
?>

In this case, we have a repository database, for example: MySQL. But suddenly we want to load our data on orders via API of the third-party server.

What changes do we need to make? There are several options, for example: to directly modify class methods OrderRepository, but this does not comply with the principle of opening / closing, since the class is closed to modifications, and changes to the already well working class is not desirable. So, you can inherit from OrderRepository class and override all the methods, but this solution is also not the best, because when you add a method to OrderRepository we have to add similar methods to all his successors. Therefore, to satisfy the principle of opening / closing is better to use the following solution - to establish interface IOrderSource, which will be implemented by the respective classes MySQLOrderSource, ApiOrderSource and so on.

<?php
class OrderRepository
{
    private 
$source;

    public function 
setSource(IOrderSource $source)
    {
        
$this->source $source;
    }

    public function 
load($orderID)
    {
        return 
$this->source->load($orderID);
    }
    
    public function 
save($order){/*...*/}
    public function 
update($order){/*...*/}
}

interface 
IOrderSource
{
    public function 
load($orderID);
    public function 
save($order);
    public function 
update($order);
    public function 
delete($order);
}

class 
MySQLOrderSource implements IOrderSource
{
    public function 
load($orderID);
    public function 
save($order){/*...*/}
    public function 
update($order){/*...*/}
    public function 
delete($order){/*...*/}
}

class 
ApiOrderSource implements IOrderSource
{
    public function 
load($orderID);
    public function 
save($order){/*...*/}
    public function 
update($order){/*...*/}
    public function 
delete($order){/*...*/}
}
?>

Thus, we can change the behavior of the source and accordingly to OrderRepository class, setting us right class implements IOrderSource, without changing OrderRepository class.

3. The Substitution Principle (Liskov Substitution)

Perhaps, the principle that causes the greatest difficulties in understanding. The principle says - "Objects in the program can be replaced by their heirs without changing the properties of the program." In my words, I would say so - when using the class heir, the result of the code execution should be predictable and do not change the properties of the method. There is a classic example with a hierarchy of geometric shapes and area calculations. The example of code is below.

<?php
class Rectangle
{
    protected 
$width;
    protected 
$height;

    public 
setWidth($width)
    {
        
$this->width $width;
    }

    public 
setHeight($height)
    {
        
$this->height $height;
    }

    public function 
getWidth()
    {
        return 
$this->width;
    }

    public function 
getHeight()
    {
        return 
$this->height;
    }
}

class 
Square extends Rectangle
{
    public 
setWidth($width)
    {
        
parent::setWidth($width);
        
parent::setHeight($width);
    }

    public 
setHeight($height)
    {
        
parent::setHeight($height);
        
parent::setWidth($height);
    }
}

function 
calculateRectangleSquare(Rectangle $rectangle$width$height)
{
    
$rectangle->setWidth($width);
    
$rectangle->setHeight($height);
    return 
$rectangle->getHeight $rectangle->getWidth;
}

calculateRectangleSquare(new Rectangle(), 45); // 20
calculateRectangleSquare(new Square(), 45); // 25 ???
?>

Obviously, such code is not executed as expected. But what's the problem? Is a "square" is not a "rectangle"? Yes, but in geometric terms. In terms of the same objects, the square is not a rectangle, because the behavior of the "square" object does not agree with the behavior of the "rectangle" object.

Ok, so how to solve the problem? The solution is closely related to the notion of contract design. The description of designing under the contract can take not one article, therefore we will be limited to features which concern the Liskov principle. Contract design leads to some limitations on how contracts can interact with inheritance, namely:

  • Preconditions can not be strengthened in a subclass.
  • Postconditions can not be weakened in a subclass.

4. The principle of interface separation (Interface segregation)

This principle says that "Many specialized interfaces are better than one universal". Compliance with this principle is necessary to ensure that the client classes that use or implement the interface will know only about the methods that they use, which leads to a reduction in the amount of unused code.

Let's take an example with an online store. Suppose our products can have a promotional code, a discount, they have some price, condition, etc. If this is clothing, then for it it is determined from what material is made, color and size. Let's describe the following interface:

<?php
interface IItem
{
    public function 
applyDiscount($discount);
    public function 
applyPromocode($promocode);

    public function 
setColor($color);
    public function 
setSize($size);
    
    public function 
setCondition($condition);
    public function 
setPrice($price);
}
?>

This interface is not good, because it involves too many methods. And what if our class of goods can not have discounts or promotional codes, or for it there is no sense in installing the material from which it is made (for example, for books). Thus, in order not to implement methods that are not used in each class, it is better to break the interface into several smaller ones and implement the necessary interfaces by each specific class.

<?php
interface IItem
{
    public function 
setCondition($condition);
    public function 
setPrice($price);
}

interface 
IClothes
{
    public function 
setColor($color);
    public function 
setSize($size);
    public function 
setMaterial($material);
}

interface 
IDiscountable
{
    public function 
applyDiscount($discount);
    public function 
applyPromocode($promocode);
}

class 
Book implemets IItemIDiscountable
{
    public function 
setCondition($condition){/*...*/}
    public function 
setPrice($price){/*...*/}
    public function 
applyDiscount($discount){/*...*/}
    public function 
applyPromocode($promocode){/*...*/}
}

class 
KidsClothes implemets IItemIClothes
{
    public function 
setCondition($condition){/*...*/}
    public function 
setPrice($price){/*...*/}
    public function 
setColor($color){/*...*/}
    public function 
setSize($size){/*...*/}
    public function 
setMaterial($material){/*...*/}
}
?>

5. Principle of Dependency Inversion

This principle says - "Dependencies within the system are built on the basis of abstractions. The top-level modules do not depend on the lower-level modules. Abstractions should not depend on details. Details must depend on abstractions." This definition can be shortened - "the dependencies should be based on abstractions, not details."

For example, consider the payment of the order by the buyer.

<?php
class Customer
{
    private 
$currentOrder null;

    public function 
buyItems()
    {    
        if(
is_null($this->currentOrder)){
            return 
false;
        }
        
$processor = new OrderProcessor();
        return 
$processor->checkout($this->currentOrder);    
    }

    public function 
addItem($item){
        if(
is_null($this->currentOrder)){
            
$this->currentOrder = new Order();
        }
        return 
$this->currentOrder->addItem($item);
    }
    
    public function 
deleteItem($item){
        if(
is_null($this->currentOrder)){
            return 
false;
        }
        return 
$this->currentOrder ->deleteItem($item);
    }
}

class 
OrderProcessor
{
    public function 
checkout($order){/*...*/}
}

?>

Everything seems quite logical. But there is a one problem - the Customer class depends on the OrderProcessor class (moreover, the principle of openness/closure is not fulfilled). In order to get rid of the dependence on a particular class, you need to make sure that Customer depends on abstraction, ie. From the IOrderProcessor interface. This dependency can be implemented through the setters, method parameters, or the Dependency Injection container. I decided to stop on method 2 and get the following code.

<?php
class Customer
{
    private 
$currentOrder null;

    public function 
buyItems(IOrderProcessor $processor)
    {    
        if(
is_null($this->currentOrder)){
            return 
false;
        }
        
        return 
$processor->checkout($this->currentOrder);    
    }

    public function 
addItem($item){
        if(
is_null($this->currentOrder)){
            
$this->currentOrder = new Order();
        }
        return 
$this->currentOrder->addItem($item);
    }
    public function 
deleteItem($item){
        if(
is_null($this->currentOrder)){
            return 
false;
        }
        return 
$this->currentOrder ->deleteItem($item);
    }
}

interface 
IOrderProcessor
{
    public function 
checkout($order);
}

class 
OrderProcessor implements IOrderProcessor
{
    public function 
checkout($order){/*...*/}
}
?>

So now, the Customer class now depends only on the abstraction, and the specific implementation, i.e. details, it is not so important.

Conclusion

Summarizing all of the above, I would like to make the following cheat sheet

  • The principle of a single responsibility
    "One object must be assigned to each facility"
    To do this, we check how many reasons we have for changing the class-if there is more than one, then we must break this class.
  • The principle of open-closedness
    "Software entities must be open for expansion, but they are closed for modification"
    For this, we represent our class as a "black box" and see if we can change its behavior in this case.
  • The substitution principle of Liskov substitution
    "Objects in the program can be replaced by their heirs without changing the properties of the program"
    For this, we check whether we have strengthened the preconditions and whether the postcondition has weakened. If this happened, then the principle is not observed
  • The principle of interface separation (Interface segregation)
    "Many specialized interfaces are better than one universal"
    We check how much the interface contains methods and how different functions are superimposed on these methods, and if necessary, we break the interfaces.
  • The principle of Dependency Invertion
    "Dependencies should be built on abstractions, not details"
    We check whether the classes depend on some other classes (instantly instantiate objects of other classes, etc.) and if this relationship takes place, we replace it with a dependence on abstraction.



Comments


Please post only comments related to the original tutorial. Be polite and helpful, do not spam or offend others.
Create Your Free Account
Please remember that this information is essential to use our services correctly.
After creating the account you will be able to download all of our FREE products.
Fields marked with * are mandatory






Please send me information about updates, new products, specials and discounts from ApPHP!
We recommend that your password should be at least 6 characters long and should be different from your username/email. Please use only letters of the English alphabet to enter your name.

Your e-mail address must be valid. We use e-mail for communication purposes (order notifications, etc). Therefore, it is essential to provide a valid e-mail address to be able to use our services correctly.

All your private data is confidential. We will never sell, exchange or market it in any way. Please refer to Privacy Policy.

By clicking "Create Account", you are indicating that you have read and agree to the ApPHP Terms & Conditions.

Quick Registration with: Facebook / Google