L — Liskov Substitution Principle: subtypes must be substitutable
Concept
The Liskov Substitution Principle (LSP) was formulated by Barbara Liskov in 1987: if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of the program.
In plain terms: if your code works with a PaymentGateway, it must still work correctly when you substitute any concrete implementation of PaymentGateway — whether that is StripeGateway, PayPalGateway, or a test FakeGateway. The caller should never need to check what concrete type it has received.
LSP goes beyond interfaces. It constrains what subclasses (and interface implementations) are allowed to do. Specifically:
- Preconditions cannot be strengthened: a subclass cannot require more from its caller than the parent required
- Postconditions cannot be weakened: a subclass cannot deliver less than the parent promised
- Invariants must be preserved: anything that is always true about the parent must remain true in the subclass
- Exceptions: a subclass may only throw exceptions that the parent declared (or subtypes of them)
- Return types: a subclass's return type may be a subtype (narrower), never a supertype (broader)
The most intuitive way to detect an LSP violation is the "type check" smell: if caller code does if ($shape instanceof Circle) before using a Shape, the Circle is probably breaking the contract that Shape promised. Callers should never need to know the concrete type.
The classic academic example (Rectangle/Square) illustrates the principle perfectly: even though every square is mathematically a rectangle, a Square class that extends Rectangle violates LSP because it breaks the postcondition that you can set width and height independently.
Code Example
<?php
declare(strict_types=1);
// CLASSIC VIOLATION: Square extends Rectangle breaks LSP
class Rectangle
{
public function __construct(
protected float $width,
protected float $height
) {}
public function setWidth(float $w): void { $this->width = $w; }
public function setHeight(float $h): void { $this->height = $h; }
public function area(): float { return $this->width * $this->height; }
}
class Square extends Rectangle
{
// Square MUST keep width == height, so it breaks Rectangle's contract
public function setWidth(float $w): void
{
$this->width = $w;
$this->height = $w; // VIOLATION: modifying height when width is set
}
public function setHeight(float $h): void
{
$this->height = $h;
$this->width = $h; // VIOLATION: modifying width when height is set
}
}
// This function works correctly for Rectangle but silently breaks for Square
function testArea(Rectangle $rect): void
{
$rect->setWidth(5);
$rect->setHeight(4);
// With Rectangle: 5 * 4 = 20 ✓
// With Square: 4 * 4 = 16 ✗ — the contract is broken
assert($rect->area() === 20.0);
}
testArea(new Rectangle(0, 0)); // passes
testArea(new Square(0)); // fails — LSP violated
// CORRECT: Use composition and a proper hierarchy
interface Shape
{
public function area(): float;
}
final class Rectangle implements Shape
{
public function __construct(
private readonly float $width,
private readonly float $height
) {}
public function area(): float { return $this->width * $this->height; }
}
final class Square implements Shape
{
public function __construct(private readonly float $side) {}
public function area(): float { return $this->side ** 2; }
}
// Now callers only depend on Shape::area() — any Shape can substitute any other
function printArea(Shape $shape): void
{
echo "Area: " . $shape->area() . "\n";
}
printArea(new Rectangle(5, 4)); // Area: 20
printArea(new Square(4)); // Area: 16 — correct and expected
// REAL-WORLD VIOLATION: Exception strengthening
interface OrderRepository
{
/** @throws OrderNotFoundException */
public function findById(int $id): Order;
}
class DatabaseOrderRepository implements OrderRepository
{
public function findById(int $id): Order
{
$order = Order::find($id);
if (!$order) {
throw new OrderNotFoundException("Order {$id} not found");
}
return $order;
}
}
// VIOLATION: This subclass throws a DIFFERENT exception the caller doesn't expect
class CachedOrderRepository implements OrderRepository
{
public function findById(int $id): Order
{
// Throws CacheConnectionException instead of OrderNotFoundException
// Callers only know to catch OrderNotFoundException — VIOLATION
return Cache::remember("order:{$id}", 3600, fn() => Order::findOrFail($id));
}
}Interview Q&A
Q: What is the Rectangle/Square problem and why does it matter beyond academia?
The Rectangle/Square problem demonstrates that inheritance should model behavioral substitutability, not just taxonomic relationships. Mathematically, every square is a rectangle — but in software, a Square that extends Rectangle breaks the postcondition that width and height are independent. The real-world version of this is any time you use inheritance because two things sound similar but have different behavioral contracts. Examples in PHP/Laravel: a ReadOnlyRepository that extends a WritableRepository but throws UnsupportedOperationException on save() — callers of WritableRepository will break. The principle forces you to think about behavior, not just is-a relationships.
Q: How does LSP interact with PHP's type system and return type covariance?
PHP 7.4+ supports covariant return types and contravariant parameter types, which is exactly what LSP requires. A child class can return a narrower type (subtype) than the parent — that is safe, because callers of the parent interface expect at minimum the parent type and get something more specific. A child class can accept a broader type (supertype) as a parameter — that is safe too, because it is more permissive than the parent required. What LSP forbids is the opposite: returning a broader type (breaking postconditions) or requiring a narrower parameter type (strengthening preconditions). PHP's type system enforces this at the language level since 7.4, which is one reason strict typing in PHP is so valuable.
Q: How do you spot LSP violations during a code review?
Four red flags: (1) instanceof checks inside a method that accepts an interface — the caller is doing type-checking that should not be necessary; (2) a concrete class that throws \RuntimeException or UnsupportedOperationException on a method defined by the interface; (3) a subclass that ignores or silently discards parameters that the parent contract requires; (4) a subclass that returns null when the parent contract promises a non-null value. Any of these means the substitution is not safe and the LSP is broken.