SOLID Principles for WordPress Development
Liskov Substitution Principle in WordPress
Subtypes must be substitutable for their base types without breaking the program
The Liskov Substitution Principle (LSP) ensures that child classes can replace parent classes without causing bugs or unexpected behavior. If you pass a subclass where a parent class is expected — everything must still work correctly.
Safe Substitution
Safe Substitution
Reliable Polymorphism
What LSP Requires
If B is a subtype of A, then objects of type A may be replaced with objects of type B without altering any of the desirable properties of the program:
Same method signatures and return types
Preconditions can only be weakened in subtypes
Postconditions can only be strengthened
No new exceptions not in parent contract
LSP Violation Signs
Watch out for these red flags that indicate a broken substitution contract:
Child class throws exceptions parent never throws
Overridden method does less than the parent
Client code checks instanceof before calling
Subclass changes observable behavior of parent
The Classic Rectangle / Square Problem
The most famous LSP example — why “is-a” in the real world doesn’t always mean inheritance in code
Violating LSP
Square “is-a” Rectangle — but breaks its contract
// ❌ Violating LSP - Square breaks Rectangle contract
class Rectangle {
protected $width;
protected $height;
public function setWidth($w) { $this->width = $w; }
public function setHeight($h) { $this->height = $h; }
public function getArea() {
return $this->width * $this->height;
}
}
class Square extends Rectangle {
// Square forces equal sides - changes parent behavior!
public function setWidth($w) {
$this->width = $w;
$this->height = $w; // unexpected side effect
}
public function setHeight($h) {
$this->width = $h;
$this->height = $h; // unexpected side effect
}
}
// Client code breaks with Square:
function resize(Rectangle $r) {
$r->setWidth(5);
$r->setHeight(3);
// Expected: 15, but Square returns 9!
assert($r->getArea() === 15);
}
PROBLEMS:
setWidth() silently changes height
Client assumptions about Rectangle are broken
Bug is invisible — no exception is thrown
Hard to detect in large codebases
Following LSP
Separate implementations of a shared interface
// ✅ Following LSP - separate hierarchy
interface Shape {
public function getArea(): float;
}
class Rectangle implements Shape {
public function __construct(
private float $width,
private float $height
) {}
public function getArea(): float {
return $this->width * $this->height;
}
}
class Square implements Shape {
public function __construct(
private float $side
) {}
public function getArea(): float {
return $this->side ** 2;
}
}
// Client works with ANY Shape safely:
function printArea(Shape $shape) {
echo $shape->getArea(); // always correct!
}
BENEFITS:
Each shape manages its own contract;
printArea() works with any Shape
Adding Triangle? Just implement Shape
No hidden side effects
WordPress Examples: Violation vs Compliance
Real WordPress scenarios where LSP matters
Violating LSP
Child class throws instead of returning
// ❌ LSP violation - Custom_Post_Type breaks WP_Post contract
class WP_Post {
public function get_title(): string {
return get_the_title($this->ID);
}
public function get_permalink(): string {
return get_permalink($this->ID);
}
}
class External_Post extends WP_Post {
// VIOLATION: throws exception instead of returning string
public function get_permalink(): string {
throw new \Exception('External posts have no permalink!');
}
}
// Code that worked fine with WP_Post now breaks:
function render_post_link(WP_Post $post) {
// Crashes when External_Post is passed!
echo '<a href="' . $post->get_permalink() . '">';
echo $post->get_title() . '</a>';
}
PROBLEMS:
render_post_link() crashes with External_Post; caller must add instanceof checks
Following LSP
Shared interface, separate adapters
// ✅ LSP compliant - proper abstraction
interface Post_Interface {
public function get_title(): string;
public function get_permalink(): string;
public function get_content(): string;
}
class WP_Post_Adapter implements Post_Interface {
public function __construct(private int $post_id) {}
public function get_title(): string {
return get_the_title($this->post_id);
}
public function get_permalink(): string {
return get_permalink($this->post_id);
}
public function get_content(): string {
return get_the_content(null, false, $this->post_id);
}
}
class External_Post_Adapter implements Post_Interface {
public function __construct(private array $data) {}
public function get_title(): string {
return $this->data['title'] ?? '';
}
public function get_permalink(): string {
return $this->data['url'] ?? '#'; // always returns string!
}
public function get_content(): string {
return $this->data['body'] ?? '';
}
}
// Works perfectly with ANY implementation:
function render_post_link(Post_Interface $post) {
echo '<a href="' . $post->get_permalink() . '">';
echo esc_html($post->get_title()) . '</a>';
}
BENEFITS:
Both adapters return safe values; render_post_link() works universally
Violating LSP
Child widget overrides and breaks cache contract
class Realtime_Widget extends Base_Widget {
public function get_cached_output($id) {
throw new \Exception('No cache!');
}
}
// Callers may crash
PROBLEMS:
Code calling get_cached_output() may crash; inheritance used for reuse, not “is-a”
Following LSP
Separate hierarchy — no broken contract
interface Widget_Cache_Interface { ... }
class Cached_Widget implements Widget_Cache_Interface { ... }
class Realtime_Widget extends WP_Widget {
// Own contract, no cache; no LSP violation
}
BENEFITS:
Realtime widget has its own clean contract; no unexpected exceptions
Frequently Asked Questions
A: Ask yourself: “Can I replace every use of the parent class with this child class, without the tests failing or behavior changing?” If yes — you’re good. If any test breaks or you need to add instanceof checks — LSP is violated.
A: No! Overriding is fine as long as the contract is preserved. You can make behavior more specific, optimize performance, or add features — but you cannot break the promises the parent made (same return types, no new exceptions, same preconditions).
A: Filters are a great example of LSP — any callback passed to apply_filters() must accept and return the same type. If a filter expects a string and you return an array, you’ve violated the contract — exactly like LSP.
A: Yes, using inheritance purely for code reuse (without a real “is-a” relationship) is a warning sign. It often leads to LSP violations. Use traits or composition instead for code reuse, and reserve inheritance for true behavioral subtypes.
A: When you extend WP_Widget, your widget must implement widget(), form(), and update() correctly. If you override update() to return false always, you break WordPress’s expectation — an LSP violation.
Official WordPress Resources
Learn how OCP aligns with WordPress plugin and theme development standards