Solid Principles in PHP – Liskov Substitution

The L in SOLID stands for Liskov Substitution and the following explanation is confusing and will fly right over your head:

Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

Let’s simplify this so that you understand it. Derived classes must be substitutable for their base classes. Meaning, every time you prepare a subclass, that subclass should be substituable in every place where the original class was accepted.

This principle states that if I have class A and we had a function called fire, then if we had a subclass, class B, according to this principle that we should be able to use class B anywhere where class A is accepted.

class A
{
    public function fire() {}
}

class B extends A
{

     public function fire() {}
}

function doSomething(A $obj)
{
    //do something with it
}

So if we call a function doSomething and it accepts A, we should be able to swap in or substitute class B and everything should continue to work perfectly.

Let’s look at another example let’s say we have a VideoPlayer classs which plays a video file and let’s add an AviVideoPlayer which extends the VideoPlayer class.

class VideoPlayer
{
    public function play($file)
    {

    }
}

class AviVideoPlayer extends VideoPlayer
{
    public function play($file)
    {
        if (pathinfo($file, PATHINFO_EXTENSION) !== 'avi') {
            throw new Exception; //violates the LSP
        }
    }
}

The AviVideoPlayer class violates the Liskov Substitution Principle. One of the rules is that the pre-conditions for the subclass can’t be greater. In this case, no longer can we substitute class AviVideoPlayer with VideoPlayer or anywhere else because the output can potentially be different. This is not the case here. If we were to use the AviVideoPlayer and the extension does not match, an exception is thrown and that’s what causes the violation.

As you can hopefully see is that this principle protects us against those situations where some kind of decsendant exposes a behaviour that’s quite different from the original parent class, abstraction or interface.

You have heard many times to code to an interface, as this ensures classes are adhering to a contract. Even with a contract in place, you can still violate the Liskov Substitution principle. Here is a example:

interface LessonRepositoryInterface
{
    public function getAll();
}

class FileLessonRepository implements LessonRepositoryInterface
{

    public function getAll()
    {
        return [];
    }
}

class DBLessonRepository implements LessonRepositoryInterface
{

    public function getAll()
    {
        return Lesson::all();
    }
}

The DBLessonRepository getAll() function is returning a collection and the FileLessonRepository getAll() function is returning an array. This violates the Liskov Substitution principle. The consumer of either of these implmentations will not work identically.

To solve this problem you need to set the return type for all function to be an array:

class FileLessonRepository implements LessonRepositoryInterface
{

    public function getAll(): Array
    {
        return [];
    }
}

class DBLessonRepository implements LessonRepositoryInterface
{

    public function getAll(): Array
    {
        return Lesson::all()->toArray();
    }
}

If the return types were different you will need to implement type checking on what is returned, and if you are doing this then that is a clear signal that you are breaking one of these principles, that being the open-closed principle. So if you are breaking one principle in the SOLID methodoligy, then you are breaking other principles.

Always make sure that your outputs match to what is specified in the contract. The following is ways to adhere to the Liskov Substitution principle.

  1. Signature must match
  2. Preconditions can’t be greate
  3. Post conditions at least equal to
  4. Exception types must match

Leave a Reply

Your email address will not be published.