The Liskov substitution principle (LSP) states: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
In simple terms: A child class must behave exactly like its parent class, so it doesn't surprise the system. If your code expects a generic parent type, you should be able to pass any child variant into it without causing errors, crashes, or weird behaviors.
Keep in mind: Model your classes based on behaviours not on properties. Model your data based on properties and not on behaviours.
The Real-World Example: Think of a TV remote control that runs on standard AA batteries
- The Expectation (Parent): The remote expects a standard AA battery. It assumes the battery will fit the slot, provide 1.5 volts of power, and have a positive and negative terminal.
- Good Substitution (LSP Compliant): You run out of Duracell batteries, so you buy a Energizer battery or a cheap store-brand battery. They look a bit different, but they provide exactly 1.5V and fit the slot. The remote still works perfectly.
- Bad Substitution (LSP Violation): Imagine a company creates a "Smart AA Battery" that is the exact same physical size, but instead of outputting 1.5V continuously, it requires a smartphone app connection to turn on, or it outputs 12V. If you put it in the remote, it either won't power up or it will fry the remote's circuits.
Even though it is technically an "AA Battery" by shape, it violates the expected behavior of an AA battery.
❌ The Wrong Way: Subclasses that break promises (Violates LSP)
A famous technical example is the Square and Rectangle dilemma. A square is technically a rectangle in geometry, but trying to force it into inheritance breaks code logic.
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; }
}
// Square forces both sides to be equal, changing the fundamental rules
class Square extends Rectangle {
public function setWidth($width) {
$this->width = $width;
$this->height = $width; // Modifies height unexpectedly!
}
public function setHeight($height) {
$this->width = $height; // Modifies width unexpectedly!
$this->height = $height;
}
}
Why this breaks the system:
If a developer writes a function expecting a standard Rectangle, they assume changing the width won't touch the height.
function render(Rectangle $rectangle) {
$rectangle->setWidth(10);
$rectangle->setHeight(5);
// If you pass a Rectangle: 10 * 5 = 50 (Correct)
// If you pass a Square: 5 * 5 = 25 (❌ Broken! The width got silently overwritten)
echo $rectangle->getArea();
}
✅The Right Way: Proper Abstraction (Adheres to LSP)
To fix this, child classes should never change the rules of the parent. If they behave differently, they shouldn't share the same direct inheritance line for those specific properties.
interface Shape {
public function getArea(): float;
}
class Rectangle implements Shape {
public function __construct(protected float $width, protected float $height) {}
public function getArea(): float { return $this->width * $this->height; }
}
class Square implements Shape {
public function __construct(protected float $side) {}
public function getArea(): float { return $this->side * $this->side; }
}
Now, you can substitute Rectangle or Square anywhere a Shape is expected, and the getArea() method will always return the correct result without any unexpected side effects.
Why this helps you
- Polymorphism: Enables the use of polymorphic behavior, making code more flexible and reusable.
- Reliability: Ensures that subclasses adhere to the contract defined by the superclass.
- Predictability: Guarantees that replacing a superclass object with a subclass object won't break the program.
The Golden Rules to avoid breaking LSP
- Don't throw unexpected exceptions: If the parent method doesn't throw errors, your child method shouldn't throw a MethodNotImplementedException.
- Don't weaken preconditions: A child class cannot demand more strict inputs than the parent class.
- Don't strengthen postconditions: A child class must return the same type of output data expected by the parent.
The 3 Main Confusions Cleared Up
1. Confusion: "If Class B passes the instanceof check for Class A, isn't it LSP compliant?"
- Reality: No. Your PHP compiler only checks syntax, not behavior. If your parent class returns an array, and your child class returns null or throws a NotImplementedException, it passes PHP's syntax checks but completely destroys LSP at runtime.
2. Confusion: "Does LSP mean child classes cannot add new methods?"
- Reality: Child classes can add as many new methods as they want. LSP only cares about the inherited methods. The child must not break the existing promises made by the parent methods.
3. Confusion: "Real-world relationships map directly to code inheritance, right?"
- Reality: This is the biggest trap in programming. In the real world, an Ostrich is a Bird. But in code, if your Bird class has a fly() method, making Ostrich extends Bird will force you to break LSP because ostriches cannot fly. Code relationships are based on what objects can DO, not what they ARE.
The Ultimate LSP Cheat Sheet (The 3 Violations)
You are violating LSP if your child class does any of these three things:
| The Violation | What it looks like in PHP | Why it breaks the app |
|---|---|---|
| 1. The Slacker | Throws a NotImplementedException or returns false for an inherited method. | Code expecting the parent class will crash unexpectedly. |
| 2. The Secret Agent | Requires a completely different, stricter type of input than the parent. | Code passing valid inputs to the parent will fail on the child. |
| 3. The Traitor | Changes or strips out the data type that the parent promised to return. | Code trying to read the result will trigger an error. |
The 3-Step Decision Matrix (How to Choose)
When writing a subclass or implementing an interface in PHP, use this mental map to ensure LSP compliance:
START LSP_Design_Check
IF Child_Disables_Or_Throws_Error_For_Inherited_Method IS True THEN
EXECUTE Stop_Do_Not_Use_Inheritance_Use_Composition_Instead
ELSE
IF Child_Changes_Expected_Type_Of_Inputs_Or_Outputs IS True THEN
EXECUTE Fix_Your_Types_To_Match_The_Parent_Exactly
ELSE
EXECUTE Leave_It_Alone_LSP_Compliant
END IF
END IF
END LSP_Design_Check
- The "Throw-Away" Test: Am I about to write a method in my child class that just says throw new Exception("Not supported")? If yes, do not extend that parent.
- The "If Instanceof" Smell: Look at your controller code. Do you ever see blocks like this?
if ($payment instanceof CryptoPayment) {
$payment->connectWallet(); // ❌ LSP Smell!
}
$payment->process();
If you have to explicitly check which child class you are holding to execute unique logic, your abstraction is broken. Move that unique logic inside the process() method itself.
- Composition Over Inheritance: If you want Class B to reuse code from Class A, but Class B doesn't share the exact same behaviors, do not use extends. Instead, pass Class A into Class B's constructor (Composition) or use a PHP trait.
Summary for Decision Making
If your code handles a parent class, it should never care, know, or suspect if it is actually holding a child class. The swap should be completely invisible.
The Laravel Blueprint: Mastering LSP
Let's look at how a Junior developer violates LSP in Laravel versus how a Master architectures it using Laravel's Cache system.
❌ The Junior Approach (Violates LSP)
Imagine your app uses Redis for fast caching, but for local development, you use simple File caching. A junior developer creates a custom wrapper but changes the behavioral rules of the output.
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class CustomCacheService
{
public function getUserData(string $key)
{
$data = Cache::get($key);
// ❌ LSP VIOLATION SMELL!
// Redis returns an Object, but File cache returns a raw String.
// The developer has to write conditional logic depending on the driver!
if (config('cache.default') === 'redis') {
return json_decode($data);
}
return $data;
}
}
Why this breaks Laravel Mastery: The application code now cares about which driver is running behind the scenes. If you switch drivers in your .env file, your code logic breaks.
The Laravel Mastery Approach (Adheres to OCP & LSP)
Laravel provides the Illuminate\Contracts\Cache\Repository contract. Any cache driver Laravel uses (Redis, Memcached, Database, File) must strictly adhere to the behavior defined by this contract.
No matter which driver is swapped in via the Service Container, it must return exactly what is expected without requiring if/else checks in your Controller.
Step 1: Trusting the Contract in your Controller
Your controller type-hints the Laravel Contract. It does not care if the data is coming from a hard drive or an in-memory Redis cluster.
namespace App\Http\Controllers;
use Illuminate\Contracts\Cache\Repository as CacheContract;
use App\Models\User;
class UserController extends Controller
{
// Laravel automatically injects whatever driver is active in config/cache.php
public function __construct(
protected CacheContract $cache
) {}
public function show($id)
{
// LSP Guarantee: Every single Laravel cache driver MUST return
// the item if found, or NULL if it doesn't exist.
// No driver is allowed to throw a "FileNotFoundException".
$user = $this->cache->remember("user:{$id}", 3600, function () use ($id) {
return User::findOrFail($id);
});
return response()->json($user);
}
}
Step 2: Creating a Custom Driver Safely (Abiding by LSP)
If your company builds a custom internal caching system (e.g., Couchbase) and you want to integrate it into Laravel, your custom driver must follow the rules of the parent Repository contract.
namespace App\Extensions;
use Illuminate\Contracts\Cache\Repository;
class CouchbaseCacheDriver implements Repository
{
// You MUST implement all methods, and they MUST behave identical to Redis/File
public function get($key, $default = null)
{
$value = $this->couchbase->find($key);
// LSP Rule: If missing, return the default value (null),
// do not throw an error or return false!
return $value ?? $default;
}
// ... implement other required contract methods
}
Summary for Laravel Developers
When writing extensions, packages, or custom drivers in Laravel, ask yourself: "If I swap this implementation in my .env or Service Provider, will I have to change a single line of code inside my Controllers or Jobs?"
If the answer is yes, your child class has broken the behavioral promises of the parent contract, violating LSP.
Top comments (0)