paulund

SOLID Principles

SOLID Principles

SOLID is a set of principles that guide you towards better object-oriented code. They were first introduced by Robert C. Martin, also known as "Uncle Bob".

The SOLID principles are:

  • Single Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

This page covers each principle in turn.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change.

In other words, a class should have only one responsibility. This keeps your code modular and easy to maintain.

For example, consider a class that handles both the creation and validation of user accounts.

This violates the Single Responsibility Principle, as these are two separate responsibilities.

A better approach would be to create separate classes for account creation and validation.

Here's an example of a class that violates the Single Responsibility Principle:

class User
{
    public function create($data)
    {
        // Create user account
    }

    public function validate($data)
    {
        // Validate user account
    }
}

In this example, the User class handles both the creation and validation of user accounts. A better approach is to split these into separate classes:

class UserAccountCreator
{
    public function create($data)
    {
        // code to create user account
    }
}

class UserAccountValidator
{
    public function validate($data)
    {
        // code to validate user account
    }
}

Open-Closed Principle (OCP)

The Open-Closed Principle states that a class should be open for extension but closed for modification.

In other words, you should be able to add new functionality without changing existing code.

You can achieve this in PHP through inheritance and polymorphism. Create a base class that defines a set of common methods, then create subclasses that override or extend them as needed. Consider the following base class:

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

You can then create subclasses for specific shapes — a rectangle and a circle — that each override area() with the correct calculation:

class Rectangle extends Shape
{
    private $width;
    private $height;

    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function area()
    {
        return $this->width * $this->height;
    }
}

class Circle extends Shape
{
    private $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }

    public function area()
    {
        return pi() * pow($this->radius, 2);
    }
}

You can now create objects of these classes and call area() without ever modifying the base Shape class.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that subtypes must be substitutable for their base types.

In other words, if a class is a subtype of another class, you should be able to use it anywhere the base class is expected without breaking anything.

To follow this principle, ensure your subclasses do not introduce behaviour that contradicts the base class contract. This keeps your code predictable and easy to reason about.

class Rectangle
{
    protected $width;
    protected $height;

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

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

    public function getArea()
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function setWidth($width)
    {
        $this->width = $width;
        $this->height = $width;
        return $width;
    }

    public function setHeight($height)
    {
        $this->width = $height;
        $this->height = $height;
        return $height;
    }
}

In this example, Square is a subtype of Rectangle. However, it violates the Liskov Substitution Principle because setWidth() and setHeight() now return values that the base class does not, changing the expected contract.

A corrected version removes those spurious return statements:

class Square extends Rectangle
{
    public function setWidth($width)
    {
        $this->width = $width;
        $this->height = $width;
    }

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

Now Square is substitutable for Rectangle because both methods behave consistently with the base class.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use.

In other words, each interface should expose only the methods that its clients actually need.

You can achieve this in PHP by splitting large interfaces into smaller, focused ones. Here is an example of how the principle is violated:

interface Shape
{
    public function draw();
    public function resize();
    public function rotate();
}

class Circle implements Shape
{
    public function draw()
    {
        // code to draw a circle
    }

    public function resize()
    {
        // code to resize a circle
    }

    public function rotate()
    {
        // code to rotate a circle
    }
}

class Square implements Shape
{
    public function draw()
    {
        // code to draw a square
    }

    public function resize()
    {
        // code to resize a square
    }

    public function rotate()
    {
        // code to rotate a square
    }
}

class Line implements Shape
{
    public function draw()
    {
        // code to draw a line
    }

    public function resize()
    {
        // code to resize a line
    }

    public function rotate()
    {
        // code to rotate a line
    }
}

The Shape interface defines three methods: draw(), resize(), and rotate(). However, not all shapes need all three. A line, for instance, only needs draw() — it does not need to be resized or rotated.

The fix is to define separate, focused interfaces:

interface Drawable
{
    public function draw();
}

interface Resizable
{
    public function resize();
}

interface Rotatable
{
    public function rotate();
}

class Circle implements Drawable, Resizable, Rotatable
{
    public function draw()
    {
        // code to draw a circle
    }

    public function resize()
    {
        // code to resize a circle
    }

    public function rotate()
    {
        // code to rotate a circle
    }
}

class Square implements Drawable, Resizable, Rotatable
{
    public function draw()
    {
        // code to draw a square
    }

    public function resize()
    {
        // code to resize a square
    }

    public function rotate()
    {
        // code to rotate a square
    }
}

class LineSegment implements Drawable
{
    public function draw()
    {
        // code to draw a line segment
    }
}

Each class now implements only the interfaces relevant to its clients, which is exactly what the Interface Segregation Principle requires.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions.

Put simply, your code should depend on abstractions rather than concrete implementations. You can achieve this in PHP through dependency injection, which decouples your code from specific classes and makes it easier to test.

Here is an example:

interface Mailer
{
    public function send($to, $subject, $body);
}

class SmtpMailer implements Mailer
{
    private $host;
    private $port;
    private $username;
    private $password;

    public function __construct($host, $port, $username, $password)
    {
        $this->host = $host;
        $this->port = $port;
        $this->username = $username;
        $this->password = $password;
    }

    public function send($to, $subject, $body)
    {
        // code to send email using SMTP
    }
}

class UserMailer
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function sendWelcomeEmail($to, $name)
    {
        $subject = 'Welcome to our site';
        $body = 'Hi ' . $name . ', welcome to our site!';
        $this->mailer->send($to, $subject, $body);
    }
}

The SmtpMailer class is responsible for sending emails via SMTP. The UserMailer class sends a welcome email to new users, but it does not depend directly on SmtpMailer. Instead, it depends on the Mailer interface. This means you can swap in a different implementation — such as a mock mailer for testing — without touching UserMailer.

To wire this up, create an instance of SmtpMailer and inject it into UserMailer:

$mailer = new SmtpMailer('smtp.example.com', 587, '[email protected]', 'password');
$userMailer = new UserMailer($mailer);

The SOLID principles are a set of guidelines that help you design better object-oriented code. By following them, you can create code that is more modular, flexible, and maintainable.