Liskov Substitution Principle in PHP

Published On2020-05-24

Liskov Substition Principle in PHP

PHP did not start as an Object Oriented Programming language, but over the years, PHP has improved with classes, namespaces, interfaces, traits, abstract classes, and other improvements that help developers write SOLID code.

A popular misconception taught is that OOP is about reusing code. A simple and seemingly harmless example is those classic diagrams where you inherit from a parent class, and work your way down to declare everything from boats to ships to bicycles to trucks.

Object Oriented Vehicles

One perceived advantage of OOP is to reuse the code. One class can inherit from another class, which makes the properties and functions of the parent class accessible in the child class, and this child class in turn can be inherited by another grandchild class, and it goes on. A bad smell in OOP is that a child class needs tricks to override a parent class:

class views_object {}
class views_handler extends views_object {}
class views_handler_area extends views_handler {}
class views_handler_area_text extends views_handler_area{}

class views_handler_area_text_custom extends views_handler_area_text {
  // ...

  public function options_submit(&$form, &$form_state) {
    // Empty, so we don't inherit options_submit from the parent.
  }
}

This is from the Views module for Drupal 7. The views_handler_area_text_custom inherits from four parents, and at this stage down the inheritance chain, this class has to override the parent class just to make it work.

This is a simple example on how overuse of inheritance can cause problems and messy code at the end. The five SOLID principles help write better OOP code, and the Liskov Substitution Principle is an important one:

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.
-- Barbara Liskov - 1987 - Keynote on data abstractions and hierarchies

Wut?

Let's rephrase this.

Behavioral subtyping

Sub-classes should satisfy the expectations of callers accessing subclass objects through references of superclass. The Liskov Substitution Principle makes sure the callers can expect the sub-classes to behave and interact in the same way the super class does. This means one can substitute an object with an object of a sub-class, and expect it to behave the same way and fulfill its contract.

A properly structured OOP code would not just be syntactically correct, but also correct in its meaning. Making sure that the sub-classes would not result in "method not found" errors is not enough: it has to be semantically the same.

Further, classes abstract a task or a piece of information to provide semantics. This can vary from abstracting the task of sending an email, or providing a unified way to access the author and number of pages in a book.

However, the abstraction has gone too far when a sub-class of the parent email class is turned into a push notification client, or when a simple book information class can fetch information about movies.

Abstraction and semantics

The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.
-- Edsger W. Dijkstra

A properly abstracted class gives a meaning to the task. It makes it meaningful to create sub-classes to handle the same task, but in various forms.

Your email class can be abstracted to receive the recipient email address, subject, and the body, and each sub-class can take care of the implementation details, whether it is using an SMTP server, or sending it over an HTTP API. For the caller, it does not matter which transportation is used, as long as the caller can provide the information, and get feedback whether the email was sent or not.

$email = new PlainTextEmail('foo@example.com', 'Subject', 'Hi Ayesh, ...');
$smtp = new SMTPTransport('smtp.example.com', 465);
$emailer = new Emailer();
$emailer->setTransport($smtp);
$emailer->send($email);

The setTransport() method will accept any transport method. We use SMTP in here, but it can be any transportation method, as long as it fulfills its contract. You can replace the SMTPTransport class with MailGunTransport, and it is supposed to work.

Furthermore, you can replace PlainTextEmail with a sub-type that might contain HTML emails, or DKIM signed emails, but the transporter (be it SMTPTransport or MailGunTransport) can still work with that email.

Behavioral subtyping in PHP

PHP's way of enforcing this is via interfaces, abstract classes, and inheritance. Every-time a class implements an interface, or inherits from a class or an abstract class, PHP checks if the child class or implementation still fulfills its contract:

Covariance

Covariance allows a child class method to declare a return type that is a sub-type of the parent methods return type.

Consider this example:


class Image {}
class JpgImage extends Image {}

class Renderer {
     public function render(): Image;
}

class PhotoRenderer {
    public function render(): JpgImage;
}

The PhotoRenderer class fulfills the contract of Renderer class because it still returns a JpgImage object, which is a sub-type of Image. Any code that knows how to work with the Renderer class will continue to work because the return value is still instanceof the expected Image class.

PHP 8.0's Union Types can show an example of this in an easier way:


class Foo {
    public function process(): string|int;
}

class Bar extends Foo {
    public function process(): int;
}

The Bar::process() return type further narrows down the return types of its parent class, but it does not violate its contract because any caller who can handle Foo knows that int is one of the expected return types.

Contravariance

Contravariance is about the function parameters a sub-class can expect. A sub-class may increase its range of parameters, but it must accept all parameters the parent accepts.


class Foo {
    public function process(int|float $value);
}

class Bar {
    public function process(int|float|string $value);
}

This contravariance is allowed, because Bar::process method accepts all parameter types the parent method accepts. For class objects, this means the sub-class can "widen" the scope of parameters it accepts, either by extending its Union Types, or by accepting a parent class.

Invariance

Invariance simply says that property types (in PHP 7.4+) cannot be further narrowed down, or widened.

The Big Picture

  • Return types can be made narrower: Sub-classes can return sub-types or a smaller Union Types in the return type.
  • Parameters can be widened: Sub-classes must accept and handle all parameter types the parent method handles. But it can be widened to accept more types or parent types.
  • Property types cannot be changed.

PHP LSP in one chart


Further Reading

Recent Articles on PHP.Watch

All ArticlesFeed
GitAttributes for PHP Composer Projects

GitAttributes for PHP Composer Projects

How to use a `.gitattributes` file to reduce the package size of Composer packages.
Performance Impact of PHP Exceptions

Performance Impact of PHP Exceptions

A benchmark on the performance cost of throwing and handling PHP exceptions
Private Composer Repositories with GitLab

Private Composer Repositories with GitLab

How to create a private Composer repository with GitLab Package Registry.
Subscribe to PHP.Watch newsletter for monthly updates

You will receive an email on last Saturday of every month and on major PHP releases with new articles related to PHP, upcoming changes, new features and what's changing in the language. No marketing emails, no selling of your contacts, no click-tracking, and one-click instant unsubscribe from any email you receive.