D
SOLID Principles for WordPress Development
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;
}
}
Problems:
• 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
Why DIP Matters for WordPress Developers
Depend on interfaces, inject implementations. Your business logic stays clean, tests become trivial, and swapping vendors never requires rewriting your core plugin.
Testability
Inject mocks in unit tests — no WordPress environment, no DB, no mail server needed
Flexibility
Swap Mailchimp for SendGrid, MySQL for Redis — without touching a single line of business logic
Clean Architecture
High-level domain logic stays pure — no infrastructure leaking into your business rules