Ready to get started?

Check out the plugin on GitHub and start using it today.

L

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

WP_Widget Reference

Understand the WP_Widget contract you must honor when extending it

Plugin API

How WordPress uses contracts (filter return types) that respect LSP

PHP Standards

PSR standards and PHP type declarations that enforce LSP contracts

When your class hierarchy respects LSP, you can pass any subclass anywhere the parent is expected — and everything just works. No surprises, no crashes, no instanceof spaghetti.

Subtypes behave consistently — no hidden surprises when swapping implementations

Test with the parent contract — all subtypes pass the same tests automatically

Write code once against an interface — swap implementations freely without touching callers