Ready to get started?

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

D

Dependency Inversion Principle in WordPress

Depend on abstractions, not on concrete implementations

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules — both should depend on abstractions. This makes your code flexible, testable, and easy to swap out implementations.

Depend on Abstractions

Swappable Implementations

Easy to Test

What DIP Requires

Two key rules define the Dependency Inversion Principle:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Use interfaces/abstract classes as the “contract” layer

Inject dependencies — don’t instantiate them inside classes

DIP Violation Signs

These patterns indicate your high-level code is too tightly coupled:

new ConcreteClass() inside business logic methods

Can’t test without a real database / email server

Changing a low-level detail breaks a high-level class

Swapping an implementation requires editing many files

The Classic Database Dependency Problem

High-level service locked to a specific database — impossible to swap or test without real infrastructure

Violating DIP

UserService hard-codes MySQL — can never be swapped

// ❌ Violating DIP - high-level depends on low-level
class MySQLDatabase {
    public function save(array $data): void {
        // MySQL-specific implementation
        $pdo = new PDO('mysql:host=localhost;dbname=wp', 'root', '');
        $stmt = $pdo->prepare("INSERT INTO posts (title) VALUES (?)");
        $stmt->execute([$data['title']]);
    }
}

// High-level UserService directly depends on MySQLDatabase
class UserService {
    private MySQLDatabase $db;

    public function __construct() {
        // Hard-coded dependency - can't swap without changing this class!
        $this->db = new MySQLDatabase();
    }

    public function create(array $data): void {
        $this->db->save($data);
    }
}

// Now try to switch to PostgreSQL or a test mock... you can't.

PROBLEMS:

• Switching to PostgreSQL requires rewriting UserService

• Testing requires a real MySQL connection

• High-level logic is polluted with DB details

Following DIP

UserService depends on an interface — any DB can be injected

// ✅ DIP compliant - depend on abstractions
interface Database_Interface {
    public function save(array $data): void;
    public function find(int $id): ?array;
    public function delete(int $id): void;
}

// Low-level modules implement the abstraction
class MySQL_Database implements Database_Interface {
    public function save(array $data): void { /* MySQL logic */ }
    public function find(int $id): ?array { /* MySQL logic */ }
    public function delete(int $id): void { /* MySQL logic */ }
}

class SQLite_Database implements Database_Interface {
    public function save(array $data): void { /* SQLite logic */ }
    public function find(int $id): ?array { /* SQLite logic */ }
    public function delete(int $id): void { /* SQLite logic */ }
}

// High-level module depends on abstraction - not concrete class
class UserService {
    public function __construct(
        private Database_Interface $db  // inject any implementation!
    ) {}

    public function create(array $data): void {
        $this->db->save($data);
    }
}

// Easily swap implementations:
$service = new UserService(new MySQL_Database());
$service = new UserService(new SQLite_Database());
$service = new UserService(new Mock_Database()); // for tests!

BENEFITS:

✓ Swap MySQL → PostgreSQL without touching UserService

✓ Inject a Mock_Database for unit tests

✓ High-level logic stays clean and focused

WordPress Examples

Real WordPress scenarios where DIP unlocks flexibility

Hard-coded Dependencies

Plugin locked to wp_mail and transients. Want Mailchimp? Rewrite the plugin.

// ❌ Plugin directly depends on specific mailer
class Newsletter_Plugin {
    public function send_newsletter(array $subscribers, string $content): void {
        // Hard-coded dependency on wp_mail
        foreach ($subscribers as $email) {
            wp_mail($email, 'Newsletter', $content);
            // What if we want to switch to Mailchimp or SendGrid?
            // We'd have to rewrite this entire method!
        }
    }
}

// Another example - hard-coded cache dependency
class Post_Service {
    public function get_popular_posts(): array {
        // Directly using WordPress transients - can't swap for Redis, etc.
        $cached = get_transient('popular_posts');
        if ($cached) return $cached;

        $posts = get_posts(['orderby' => 'comment_count', 'numberposts' => 10]);
        set_transient('popular_posts', $posts, HOUR_IN_SECONDS);
        return $posts;
    }
}

• Want to use Mailchimp? Rewrite the plugin

• Want Redis cache? Rewrite Post_Service

• Testing requires a WordPress environment

Injected Dependencies

Plugin accepts Mailer_Interface and Cache_Interface. Swap Mailchimp, Redis, or mocks for tests.

// ✅ DIP compliant WordPress plugin
interface Mailer_Interface {
    public function send(string $to, string $subject, string $body): bool;
}

interface Cache_Interface {
    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl = 3600): void;
    public function delete(string $key): void;
}

// WordPress implementations
class WP_Mailer implements Mailer_Interface {
    public function send(string $to, string $subject, string $body): bool {
        return wp_mail($to, $subject, $body);
    }
}

class WP_Cache implements Cache_Interface {
    public function get(string $key): mixed {
        return get_transient($key) ?: null;
    }
    public function set(string $key, mixed $value, int $ttl = 3600): void {
        set_transient($key, $value, $ttl);
    }
    public function delete(string $key): void {
        delete_transient($key);
    }
}

// Plugin depends on abstractions
class Newsletter_Plugin {
    public function __construct(
        private Mailer_Interface $mailer
    ) {}

    public function send_newsletter(array $subscribers, string $content): void {
        foreach ($subscribers as $email) {
            $this->mailer->send($email, 'Newsletter', $content);
        }
    }
}

class Post_Service {
    public function __construct(
        private Cache_Interface $cache
    ) {}

    public function get_popular_posts(): array {
        $cached = $this->cache->get('popular_posts');
        if ($cached) return $cached;

        $posts = get_posts(['orderby' => 'comment_count', 'numberposts' => 10]);
        $this->cache->set('popular_posts', $posts, HOUR_IN_SECONDS);
        return $posts;
    }
}

// Bootstrap - wire up real implementations
$plugin = new Newsletter_Plugin(new WP_Mailer());
$service = new Post_Service(new WP_Cache());

// For tests - inject mocks
$plugin = new Newsletter_Plugin(new Mock_Mailer());
$service = new Post_Service(new Array_Cache());

Benefits:

✓ Swap Mailchimp without touching plugin logic

✓ Inject Array_Cache for blazing-fast tests

✓ Same plugin works with different infrastructure

Lightweight DI Container

Wire up all dependencies in one place at plugin bootstrap. Swap any binding without touching business logic. In tests: override bindings with mocks.

// ✅ Dependency injection container for WordPress plugins
class Plugin_Container {
    private array $bindings = [];

    public function bind(string $abstract, callable $factory): void {
        $this->bindings[$abstract] = $factory;
    }

    public function make(string $abstract): mixed {
        if (!isset($this->bindings[$abstract])) {
            throw new \Exception("No binding for {$abstract}");
        }
        return ($this->bindings[$abstract])($this);
    }
}

// Register bindings in plugin bootstrap
class My_Plugin {
    private Plugin_Container $container;

    public function __construct() {
        $this->container = new Plugin_Container();
        $this->register_bindings();
    }

    private function register_bindings(): void {
        $this->container->bind(Cache_Interface::class, fn() => new WP_Cache());
        $this->container->bind(Mailer_Interface::class, fn() => new WP_Mailer());
        $this->container->bind(Post_Service::class, fn($c) =>
            new Post_Service($c->make(Cache_Interface::class))
        );
        $this->container->bind(Newsletter_Plugin::class, fn($c) =>
            new Newsletter_Plugin($c->make(Mailer_Interface::class))
        );
    }

    public function boot(): void {
        $service = $this->container->make(Post_Service::class);
        // use $service...
    }
}// ✅ Dependency injection container for WordPress plugins
class Plugin_Container {
    private array $bindings = [];

    public function bind(string $abstract, callable $factory): void {
        $this->bindings[$abstract] = $factory;
    }

    public function make(string $abstract): mixed {
        if (!isset($this->bindings[$abstract])) {
            throw new \Exception("No binding for {$abstract}");
        }
        return ($this->bindings[$abstract])($this);
    }
}

// Register bindings in plugin bootstrap
class My_Plugin {
    private Plugin_Container $container;

    public function __construct() {
        $this->container = new Plugin_Container();
        $this->register_bindings();
    }

    private function register_bindings(): void {
        $this->container->bind(Cache_Interface::class, fn() => new WP_Cache());
        $this->container->bind(Mailer_Interface::class, fn() => new WP_Mailer());
        $this->container->bind(Post_Service::class, fn($c) =>
            new Post_Service($c->make(Cache_Interface::class))
        );
        $this->container->bind(Newsletter_Plugin::class, fn($c) =>
            new Newsletter_Plugin($c->make(Mailer_Interface::class))
        );
    }

    public function boot(): void {
        $service = $this->container->make(Post_Service::class);
        // use $service...
    }
}

Benefits:

✓ All wiring is in one place — easy to audit

✓ Swap any binding without touching business logic

✓ In tests: override bindings with mocks in seconds

Common DIP Violations in WordPress

Patterns that tightly couple your plugin to specific implementations

new ConcreteClass() in Business Logic

Instantiating concrete classes inside service methods couples high-level logic to low-level details

Why it’s bad:

Updates overwrite your changes, breaking your site. Use hooks, filters, or child themes instead.

Direct wp_mail() in Business Classes

Calling WordPress functions directly inside domain/service classes couples logic to WP core

Why it’s bad:

Calling WordPress functions directly inside domain/service classes couples logic to WP core

Static Method Calls

Logger::log()Cache::get() as static calls — impossible to mock in tests

Why it’s bad:

Convert to instance methods and inject via constructor. Static calls are hidden dependencies that violate DIP.

Global State via get_option()

Calling get_option() directly inside classes — hidden global dependency

Why it’s bad:

Create a Plugin_Settings class implementing a Settings_Interface and inject it. Makes settings testable and swappable.

Frequently Asked Questions

A: DIP is the principle — depend on abstractions, not concretions. Dependency Injection (DI) is a technique to achieve it — passing dependencies into a class rather than creating them inside.

A: For tiny plugins (under 200 lines), it can be overkill. But once you have classes with external dependencies (email, cache, DB, APIs), DIP pays off immediately through testability and flexibility.

A: Partially. The Hooks API is an excellent abstraction layer — plugins depend on the hook system, not on core internals. But older WordPress code uses global functions and static calls heavily.

A: Not at all. A simple array-based container handles 95% of cases. For larger plugins, consider PHP-DI or Pimple. But starting with constructor injection and interfaces is enough.

Official WordPress Resources

How DIP and dependency injection apply in WordPress development

Plugin Best Practices

WordPress’s own guide recommends dependency injection for testable plugin architecture

Plugin API / Hooks

WordPress hooks are DIP in action — plugins depend on abstractions, not core internals

PSR-11: Container Interface

Standard PHP interface for DI containers — ensures containers themselves follow DIP

Depend on interfaces, inject implementations. Your business logic stays clean, tests become trivial, and swapping vendors never requires rewriting your core plugin.

Inject mocks in unit tests — no WordPress environment, no DB, no mail server needed

Swap Mailchimp for SendGrid, MySQL for Redis — without touching a single line of business logic

High-level domain logic stays pure — no infrastructure leaking into your business rules