Frank Perez

Frank Perez

Keep in touch

An experienced Web Developer with years of experience in design, programming and server administration.

Blog

Overview

© 2024 by Frank Perez.

SOLID Principles in PHP

The 5 basic principles for Object Oriented Design, SOLID, were first created in a effort to improve maintainability in our code bases. SOLID is a mnemonic acronym that stands for each of the following principles: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.

In this article, I’ll explain in short what each principle is about. I will also include an example use case for each to portray what these principles hope to accomplish.

Single Responsibility Principle (SRP)

This principle states that each class should only have a single responsibility and only one reason to change within an application.

Let’s look at an example of some code that does not really follow this principle.

<?php

class Notification
{
    private $message;

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

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

    public function sendToEmail($email)
    {
        // Code Here
    }

    public function sendToSMS($mobileNumber)
    {
        // Code Here
    }
}

The problem above, is that not only are you creating the Notification message here. You’re also expecting the same class to know how to send it to a Email, or SMS. We should separate this business logic from the presentation data.

Now we’ll see how we can update it to better follow the Single Responsibility Principle.

<?php

class Notification
{
    private $message;

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

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

interface Notifier
{
    public function send($to);
}

class EmailNotifier implements Notifier
{
    private $notification;

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

    public function send($to)
    {
        // Email Logic Here
    }
}

class SMSNotifier implements Notifier
{
    private $notification;

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

    public function send($to)
    {
        // SMS Logic Here
    }
}

Now that we’ve organized the above in this fashion, it is easy for us to create new Notifier’s without having to ever touch any of the existing classes. For instance, let’s assume we want to create a notifier capable of sending notifications to Slack. We could simply create a new class for this like so.

<?php

class SlackNotifier implements Notifier
{
    private $notification;

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

    public function send($to)
    {
        // Slack Logic Here
    }
}

Open-Closed Principle (OCP)

The entire idea of the Open-Closed Principle, is so that your classes should be easily extendible without the need of editing the classes directly.

In our previous example regarding the Notifier you saw that we were able easily add new Notification channels without ever having to modify any of the existing classes. This is exactly what this principle is trying to enforce.

One thing you’ll notice is that all of the Principles play together hand and hand.

Let’s look at another example of how this works by creating a trait for Vehicle with some predefined methods. We’ll then create a new class for each body style while including the trait.

You could also use an Abstract class to accomplish the same thing.

<?php

trait Vehicle
{
    protected $type;
    protected $make;
    protected $model;
    protected $year;

    public function type() : string
    {
        return __CLASS__;
    }

    public function make() : string
    {
        return $this->make;
    }

    public function model() : string
    {
        return $this->model;
    }

    public function year() : int
    {
        return $this->year;
    }
}

class Sedan
{
    use Vehicle;

    public function __construct(string $make, string $model, int $year)
    {
        $this->make = $make;
        $this->model = $model;
        $this->year = $year;
    }
}

class Coupe
{
    use Vehicle;

    public function __construct(string $make, string $model, int $year)
    {
        $this->make = $make;
        $this->model = $model;
        $this->year = $year;
    }
}

Notice that now we can just continue making new body styles without ever modifying any of the existing classes.

Let’s make another body style for Convertible.

<?php

class Convertible
{
    use Vehicle;

    public function __construct(string $make, string $model, int $year)
    {
        $this->make = $make;
        $this->model = $model;
        $this->year = $year;
    }
}

You see how simple it is now, to add additional body styles for our application.

Liskov Substitution Principle (LSP)

The idea behind the Liskov Substitution Principle is that a child class should never break it’s parent class. Instead we should be able to swap out any class of a same type with another while the application continues to work as normal.

Let’s add a bit more functionality to our Notification example from the first principle.

<?php

class Notify
{
    private $notifier;

    public function send(Notifier $notifier)
    {
        $this->notifier = $notifier;

        return $this;
    }

    public function to(string $to)
    {
        return $notifier->send($to);
    }
}

class Notification
{
    private $message;

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

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

interface Notifier
{
    public function send($to);
}

class EmailNotifier implements Notifier
{
    private $notification;

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

    public function send($to)
    {
        // Email Logic Here
    }
}

class SMSNotifier implements Notifier
{
    private $notification;

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

    public function send($to)
    {
        // SMS Logic Here
    }
}

You’ll see that we added Notify class that is capable of running a Notifier instance. Let me show you how’d we go about using this.

<?php

$notification = new Notification('Test Message');

$notify = new Notify();

// Send Email Notification
$notify->send(new EmailNotifier($notification))->to('[email protected]');

// Send SMS Notification
$notify->send(new SMSNotifier($notification))->to('+19999999999');

You see how easy it is to swap out the Notifier within our Notify class. This is amazing and should improve the ease of working within our code base.

Interface Segregation Principle (ISP)

Interface Segregation Principle states that no class should be forced to use methods it does not need. One way to solve this is to keep your interfaces, abstract classes, and traits small by creating multiple instead of a single large one.

Let’s take an example of a Paper Book. With a book you can flip to the next page and go to the previous page. If we’re reading on a Kindle though, you’ll be able to easily bookmark a page, apart from the regular page flipping functionality.

This is how we may handle something like this.

Keep in mind that this is a bare bones example, and there is still more that we would need to pass to actually have a functional system. Such as the actual book contents, and implementations of each method. I kept it short for the sake of this example.

<?php

interface ElectronicReader
{
    public function bookmarkPage();
}

interface Book
{
    public function nextPage();

    public function previousPage();
}

class PaperBook implements Book
{
    public function nextPage()
    {
        // TODO: Implement nextPage() method.
    }

    public function previousPage()
    {
        // TODO: Implement previousPage() method.
    }
}

class Kindle implements Book, ElectronicReader
{
    public function nextPage()
    {
        // TODO: Implement nextPage() method.
    }

    public function previousPage()
    {
        // TODO: Implement previousPage() method.
    }

    public function bookmarkPage()
    {
        // TODO: Implement bookmarkPage() method.
    }
}

Dependency Inversion Principle (DIP)

Lastly, we have the Dependency Inversion Principle which is my favorite. The idea behind this is that your classes should not know the details of how the object is setup. Instead a object should be provided to your classes to use by dependency injection to your constructor or even method. This decouples your dependencies from your code base, allowing you to easily test your code and swap out instances of a class if needed with ease.

Let’s look again at our Notifier code from earlier.

<?php

class Notify
{
    private $notifier;

    public function send(Notifier $notifier)
    {
        $this->notifier = $notifier;

        return $this;
    }

    public function to(string $to)
    {
        return $notifier->send($to);
    }
}

You’ll see that on our send method, we’re passing the name of the interface Notifier which our Notifier concrete classes will implement. Doing this allows us to easily swap out the way we send notifications without ever having to modify any of the code within our Notify class.

We we’re able to demonstrate this earlier by sending a SMS and Email Notification from the same Notify class. I’ve included the example again below.

<?php

$notification = new Notification('Test Message');

$notify = new Notify();

// Send Email Notification
$notify->send(new EmailNotifier($notification))->to('[email protected]');

// Send SMS Notification
$notify->send(new SMSNotifier($notification))->to('+19999999999');

Conclusion

Following the above principles will improve your code base and make it easier for you and other developers to maintain and extend it. Don’t worry if you’re not following these 100%. They’re only guidelines meant to help you improve the readability of your code, but not mandatory, if for some reason you do not like something about these principles, or do not agree with them. One thing I can guarantee though is that following these principles will definitely be beneficial in the long run.

The choice is yours.