Liskov Substitution Principle
Subtypes must be substitutable for their base type without altering the correctness of the program.
Problem
Square extends Rectangle because "a square is a rectangle" mathematically. But to stay square, setWidth() must also change height (and vice versa) — a side effect Rectangle never promised. Code written against Rectangle breaks silently when a Square is substituted in.
Solution
Stop forcing an inheritance relationship that requires lying about behaviour. Both Rectangle and Square implement a shared Shape interface independently, each honestly fulfilling only the contract it can actually keep.
Analogy
A remote-control toy car that's "a subtype of Car" but the steering wheel doesn't work — technically a car, but you can't drive it like every other car. If a subtype changes the rules, it isn't really substitutable.
Participants (After)
| Class | Role |
|---|---|
| Shape | The abstraction — only promises area() |
| Rectangle | Independent width/height, implements Shape directly |
| Square | One side length, implements Shape directly — no longer extends Rectangle |
Run the Demo
Activity Log
Source Code
▸ Before: Rectangle.php
<?php
declare(strict_types=1);
namespace App\SolidPrinciples\Lsp\Before;
/**
* LSP VIOLATION SETUP — an ordinary rectangle with independent
* width/height setters. Any code using Rectangle expects that
* setting width does NOT change height, and vice versa.
*/
class Rectangle
{
protected float $width = 0;
protected float $height = 0;
public function setWidth(float $width): void
{
$this->width = $width;
}
public function setHeight(float $height): void
{
$this->height = $height;
}
public function getWidth(): float
{
return $this->width;
}
public function getHeight(): float
{
return $this->height;
}
public function area(): float
{
return $this->width * $this->height;
}
}
▸ Before: Square.php
<?php
declare(strict_types=1);
namespace App\SolidPrinciples\Lsp\Before;
/**
* LSP VIOLATION — mathematically a square IS a rectangle, so it seems
* natural to extend Rectangle. But to keep a square "square", setting
* width must also change height (and vice versa) — silently breaking
* the parent's contract. Any code written against Rectangle that
* substitutes a Square here gets surprising, wrong behaviour.
*/
class Square extends Rectangle
{
public function setWidth(float $width): void
{
$this->width = $width;
$this->height = $width; // side-effect the base class never promised
}
public function setHeight(float $height): void
{
$this->width = $height; // side-effect the base class never promised
$this->height = $height;
}
}
▸ After: Shape.php
<?php
declare(strict_types=1);
namespace App\SolidPrinciples\Lsp\After;
/**
* LSP FIX — a common abstraction that makes no promises about
* width/height independence. Both Rectangle and Square honestly
* implement only what they can guarantee: an area.
*/
interface Shape
{
public function area(): float;
}
▸ After: Rectangle.php
<?php
declare(strict_types=1);
namespace App\SolidPrinciples\Lsp\After;
/**
* LSP FIX — Rectangle still has independent width/height, but it no
* longer sits in an inheritance relationship that a Square must lie
* about to fit into.
*/
class Rectangle implements Shape
{
public function __construct(
private float $width,
private float $height,
) {}
public function area(): float
{
return $this->width * $this->height;
}
}
▸ After: Square.php
<?php
declare(strict_types=1);
namespace App\SolidPrinciples\Lsp\After;
/**
* LSP FIX — Square is its OWN class implementing Shape directly. It
* makes an honest, self-contained promise: one side length, one area.
* It no longer extends Rectangle, so it can never violate Rectangle's
* contract — because it never inherited that contract in the first place.
*/
class Square implements Shape
{
public function __construct(
private float $side,
) {}
public function area(): float
{
return $this->side * $this->side;
}
}