If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSolid principle number three is, I think, a pretty cool one. It's the Liskov Substitution Principle, developed by Barbara Liskov: a researcher at MIT and winner of the Turing award, which is, I've learned, sort of the Nobel prize for computer science. No biggie.
Liskov's principle states:
Subtypes must be substitutable for their base types.
That's... actually not a terrible definition. A "subtype" basically means a class: any class that extends a base class or that implements an interface.
So let me rephrase the definition. I'm going to stick to just talking about classes and parent classes, but this applies equally to a class that implements an interface. Here it is:
You should be able to substitute a class for a sub-class without breaking your app or needing to change any code.
Dan North refers to this as simply:
The principle of least surprise, applied to classes that have a parent class or implement an interface.
In other words, a class should behave in a way that most users expect: it should behave like its parent class or interface intended.
Okay, that sounds great! But... what does that mean specifically?
It means four specific things. Pretend that we have a class that extends a base class or implements an interface. It also has a protected property and a method, both of which live in that parent class. Or in the case of the method, it lives on the interface.
Given this setup, Liskov says 4 things.
One: you cannot change the type of a protected property.
Two: you can't narrow the type hint of an argument. Like, if the parent class uses the object
type-hint, you can't make this narrower in your subclass by requiring something more specific, like a DateTime
object.
Three, which is both similar and opposite to the previous rule, you can't widen the return type. If the parent class says a method returns a DateTime
object, you can't change this in the subclass to suddenly return something wider, like any object.
And finally, four, you should follow your parent class's - or interface's - rules around whether or not you should throw an exception under certain conditions.
There may be some edge-case things that I've missed with these 4 rules, but this is the basic idea. By violating any of these rules, you are making your class behave differently than its parent class or interface intended. That's bad because if part of your code expects an instance of that interface and you pass in your class, even though it implements the interface, the class's violations may cause weird stuff to happen. We'll see specific examples of this over the next few chapters.
Now here's what I really love about this principle. Those first three rules? Yeah, they're impossible to violate in PHP. If you change the property type on a protected property, narrow the type-hint on an argument or widen a return type on a method, PHP will give you a syntax error. Yup, Liskov's principle makes so much sense, that its rules are codified right into the language.
So, we now know the rules of Liskov. But to get a deeper feeling for why these rules exist and - almost more importantly - what things we are allowed to do in a "subtype", let's jump into two real-world examples next.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.1
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2", // 2.3.1
"doctrine/doctrine-migrations-bundle": "^3", // 3.1.1
"doctrine/orm": "^2", // 2.8.4
"knplabs/knp-time-bundle": "^1.15", // v1.16.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^6.0", // v6.1.2
"symfony/console": "5.2.*", // v5.2.6
"symfony/dotenv": "5.2.*", // v5.2.4
"symfony/flex": "^1.9", // v1.18.7
"symfony/form": "5.2.*", // v5.2.6
"symfony/framework-bundle": "5.2.*", // v5.2.6
"symfony/http-client": "5.2.*", // v5.2.6
"symfony/mailer": "5.2.*", // v5.2.6
"symfony/property-access": "5.2.*", // v5.2.4
"symfony/property-info": "5.2.*", // v5.2.4
"symfony/security-bundle": "5.2.*", // v5.2.6
"symfony/serializer": "5.2.*", // v5.2.4
"symfony/twig-bundle": "5.2.*", // v5.2.4
"symfony/validator": "5.2.*", // v5.2.6
"symfony/webpack-encore-bundle": "^1.6", // v1.11.1
"symfony/yaml": "5.2.*", // v5.2.5
"twig/cssinliner-extra": "^3.3", // v3.3.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.0
"twig/twig": "^2.12|^3.0" // v3.3.0
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.2", // 3.4.0
"fakerphp/faker": "^1.13", // v1.14.1
"symfony/debug-bundle": "^5.2", // v5.2.4
"symfony/maker-bundle": "^1.13", // v1.30.2
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/stopwatch": "^5.2", // v5.2.4
"symfony/var-dumper": "^5.2", // v5.2.6
"symfony/web-profiler-bundle": "^5.2" // v5.2.6
}
}