Skip to main content
WP HealthKit

WordPress Plugin Dependency Injection: PHP Design Patterns

May 5, 202619 min readQualityBy Jamie

Table of Contents

  1. Understanding Dependency Injection
  2. Constructor Injection Patterns
  3. Building a WordPress Service Container
  4. Integrating DI with WordPress Hooks
  5. Testing with Dependency Injection
  6. Avoiding Global State
  7. Practical Refactoring Examples
  8. Container Patterns and Best Practices

Understanding Dependency Injection

Dependency injection (DI) is a design pattern that removes hard-coded dependencies from your code, making it more flexible, testable, and maintainable. Instead of a class creating its own dependencies, those dependencies are "injected" into the class from outside.

This concept seems abstract until you see how WordPress plugin development typically works. Most WordPress code—including plugins—directly instantiates its dependencies:

// Traditional WordPress approach WITHOUT dependency injection
class BlogPostManager {
    private $database;
    
    public function __construct() {
        // Hard-coded dependency - this is the problem
        $this->database = new MySQL_Database();
    }
    
    public function save_post( $title, $content ) {
        // Tightly coupled to MySQL_Database
        return $this->database->insert( 'posts', array(
            'title' => $title,
            'content' => $content
        ));
    }
}

// Usage
$manager = new BlogPostManager();
$manager->save_post( 'Hello', 'World' );

The problem: BlogPostManager is tightly coupled to MySQL_Database. If you want to test this code, you must use a real database. If you want to switch to a different database, you must modify the class. This violates the Single Responsibility Principle.

With dependency injection, dependencies are provided externally:

// Dependency Injection approach - BETTER
class BlogPostManager {
    private $database;
    
    // Dependencies are injected through the constructor
    public function __construct( Database $database ) {
        $this->database = $database;
    }
    
    public function save_post( $title, $content ) {
        return $this->database->insert( 'posts', array(
            'title' => $title,
            'content' => $content
        ));
    }
}

// Interface defining the contract
interface Database {
    public function insert( $table, $data );
}

// Real implementation
class WordPress_Database implements Database {
    public function insert( $table, $data ) {
        global $wpdb;
        return $wpdb->insert( $wpdb->prefix . $table, $data );
    }
}

// Mock implementation for testing
class Mock_Database implements Database {
    public function insert( $table, $data ) {
        // Return mock data for testing
        return array( 'success' => true, 'id' => 123 );
    }
}

// Usage with real database
$database = new WordPress_Database();
$manager = new BlogPostManager( $database );

// Usage with mock database in tests
$database = new Mock_Database();
$manager = new BlogPostManager( $database );
$manager->save_post( 'Test', 'Content' ); // Uses mock, no real database needed

Dependency injection transforms code structure from rigid and tightly coupled to flexible and testable. By depending on abstractions (interfaces) rather than concrete implementations, your code becomes resilient to changes in implementation details. When WordPress adds new database methods or you want to add caching, you implement a new Database class without touching the BlogPostManager class. This is the Open/Closed Principle: code should be open for extension but closed for modification. Dependency injection enables this pattern naturally. Additionally, dependency injection makes it obvious what a class depends on by examining its constructor. Instead of hiding dependencies inside the constructor or in static initialization code, they're explicit parameters. This "dependency transparency" makes code easier to understand and reason about.

Constructor injection is the most common pattern, but other injection methods exist. Setter injection allows dependencies to be set after construction, useful for optional dependencies. Property injection directly assigns dependencies to class properties. Method injection provides dependencies as parameters to specific methods. Choosing the right injection method depends on whether dependencies are required or optional and how often they might change. For most WordPress plugins, constructor injection works perfectly and should be your default choice. Only use other patterns if you have specific reasons for them. $manager->save_post( 'Hello', 'World' );

// Usage with mock for testing $mock_database = new Mock_Database(); $manager = new BlogPostManager( $mock_database ); // Now tests don't touch the real database


The benefits are immediate: `BlogPostManager` no longer knows about database implementations. You can test it with mocks. You can swap database implementations by just changing which class you inject. This is the power of dependency injection.

WordPress plugin development rarely uses DI, which contributes to many quality problems WP HealthKit detects. Plugins without DI are difficult to test, hard to maintain, and prone to bugs because tight coupling makes changes risky.

## Constructor Injection Patterns

Constructor injection is the most common DI pattern. Dependencies are required parameters in the constructor, making it clear what a class needs to function.

**Basic Constructor Injection:**

```php
// Before: Hard-coded dependencies
class UserManager {
    private $wpdb;
    private $cache;
    
    public function __construct() {
        global $wpdb;
        $this->wpdb = $wpdb;
        // Hard-coded global access
        $this->cache = wp_cache_get( 'users' );
    }
}

// After: Constructor injection
class UserManager {
    private $wpdb;
    private $cache;
    
    public function __construct( wpdb $wpdb, Cache $cache ) {
        $this->wpdb = $wpdb;
        $this->cache = $cache;
    }
    
    public function get_user( $user_id ) {
        // Use injected dependencies
        $cached = $this->cache->get( "user_{$user_id}" );
        if ( $cached ) {
            return $cached;
        }
        
        $user = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT * FROM {$this->wpdb->users} WHERE ID = %d",
                $user_id
            )
        );
        
        $this->cache->set( "user_{$user_id}", $user );
        return $user;
    }
}

Multi-level Dependency Injection:

Real applications have complex dependency hierarchies. A single class may need multiple dependencies, which themselves have dependencies:

// Define interfaces for dependencies
interface Cache {
    public function get( $key );
    public function set( $key, $value );
}

interface Logger {
    public function log( $message, $level );
}

interface Database {
    public function query( $sql );
}

// Classes with their own dependencies
class UserManager {
    private $database;
    private $cache;
    private $logger;
    
    public function __construct( Database $database, Cache $cache, Logger $logger ) {
        $this->database = $database;
        $this->cache = $cache;
        $this->logger = $logger;
    }
    
    public function get_user( $user_id ) {
        $this->logger->log( "Fetching user $user_id", 'debug' );
        
        $cached = $this->cache->get( "user_{$user_id}" );
        if ( $cached ) {
            return $cached;
        }
        
        $user = $this->database->query( "SELECT * FROM users WHERE ID = $user_id" );
        $this->cache->set( "user_{$user_id}", $user );
        
        return $user;
    }
}

class PostManager {
    private $database;
    private $user_manager;
    private $logger;
    
    public function __construct( Database $database, UserManager $user_manager, Logger $logger ) {
        $this->database = $database;
        $this->user_manager = $user_manager;
        $this->logger = $logger;
    }
    
    public function create_post( $user_id, $title, $content ) {
        $user = $this->user_manager->get_user( $user_id );
        
        if ( ! $user ) {
            $this->logger->log( "Invalid user $user_id", 'error' );
            return false;
        }
        
        $post_id = $this->database->query(
            "INSERT INTO posts (user_id, title, content) VALUES ($user_id, '$title', '$content')"
        );
        
        $this->logger->log( "Post $post_id created", 'info' );
        return $post_id;
    }
}

Managing these hierarchies manually gets complex quickly, which is where service containers help.

Building a WordPress Service Container

A service container is a central registry that creates and manages object instances. Instead of manually instantiating classes with their dependencies, you ask the container for an object, and it handles the dependency chain. Service containers are the infrastructure that makes dependency injection practical at scale. As your plugin grows and adds more classes with complex dependencies, manually instantiating everything becomes unwieldy. A service container automates this process: you tell it how to create each service, and it handles the creation and wiring automatically. Containers also enable powerful patterns like lazy loading (services are created only when requested) and singletons (services are created once and reused). WordPress plugins commonly use containers from popular PHP frameworks like Laravel's Container or Symfony's DependencyInjection component. These mature containers support advanced features like automatic constructor parameter resolution, service aliasing, and configuration management. For simpler plugins, a custom lightweight container might be sufficient. The key is choosing based on your needs: a small plugin might not need a container at all, while a large plugin with dozens of classes becomes much more maintainable with a container managing dependencies.

Simple WordPress Service Container:

class WP_HealthKit_Container {
    private $bindings = array();
    private $instances = array();
    private $singletons = array();
    
    /**
     * Register a binding in the container
     * 
     * @param string $name The service name
     * @param callable $resolver Function that creates the service
     * @param bool $singleton Whether to cache the instance
     */
    public function bind( $name, callable $resolver, $singleton = false ) {
        $this->bindings[$name] = array(
            'resolver' => $resolver,
            'singleton' => $singleton
        );
    }
    
    /**
     * Resolve a service from the container
     */
    public function make( $name ) {
        if ( ! isset( $this->bindings[$name] ) ) {
            throw new Exception( "Service '$name' not found in container" );
        }
        
        $binding = $this->bindings[$name];
        
        // Return cached singleton
        if ( $binding['singleton'] && isset( $this->instances[$name] ) ) {
            return $this->instances[$name];
        }
        
        // Resolve the service
        $instance = call_user_func( $binding['resolver'], $this );
        
        // Cache if singleton
        if ( $binding['singleton'] ) {
            $this->instances[$name] = $instance;
        }
        
        return $instance;
    }
    
    /**
     * Register a singleton (cached) service
     */
    public function singleton( $name, callable $resolver ) {
        $this->bind( $name, $resolver, true );
    }
    
    /**
     * Check if a service is registered
     */
    public function has( $name ) {
        return isset( $this->bindings[$name] );
    }
}

// Usage
$container = new WP_HealthKit_Container();

// Register services
$container->bind( 'database', function( $container ) {
    global $wpdb;
    return new WordPress_Database( $wpdb );
});

$container->singleton( 'cache', function( $container ) {
    return new WordPress_Cache();
});

$container->singleton( 'logger', function( $container ) {
    return new File_Logger( WP_CONTENT_DIR . '/logs/plugin.log' );
});

// Register complex service that depends on others
$container->singleton( 'user-manager', function( $container ) {
    return new UserManager(
        $container->make( 'database' ),
        $container->make( 'cache' ),
        $container->make( 'logger' )
    );
});

// Get a fully-instantiated service with all dependencies
$user_manager = $container->make( 'user-manager' );

Integrating DI with WordPress Hooks

WordPress hooks are the standard way plugins interact with WordPress and each other. Integrating DI with hooks requires careful patterns to avoid recreating globals.

Initialize Plugin with Container:

<?php
/**
 * Plugin Name: WP HealthKit Plugin
 * Description: Example plugin using dependency injection
 * Version: 1.0.0
 */

// Don't let WordPress load if run directly
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Define container as global once
global $wp_healthkit_container;

if ( ! isset( $wp_healthkit_container ) ) {
    $wp_healthkit_container = new WP_HealthKit_Container();
    
    // Configure the container
    require_once plugin_dir_path( __FILE__ ) . 'config/container.php';
}

// Initialize plugin
add_action( 'plugins_loaded', function() {
    global $wp_healthkit_container;
    
    // Get the main plugin class from container
    $plugin = $wp_healthkit_container->make( 'plugin' );
    $plugin->initialize();
} );

Container Configuration:

// config/container.php

global $wp_healthkit_container;

// Service registrations
$wp_healthkit_container->singleton( 'database', function( $c ) {
    global $wpdb;
    return new WP_HealthKit\Database\WordPress_Database( $wpdb );
});

$wp_healthkit_container->singleton( 'cache', function( $c ) {
    return new WP_HealthKit\Cache\WordPress_Cache();
});

$wp_healthkit_container->singleton( 'logger', function( $c ) {
    return new WP_HealthKit\Logger\File_Logger(
        WP_CONTENT_DIR . '/logs/wp-healthkit.log'
    );
});

$wp_healthkit_container->singleton( 'settings', function( $c ) {
    return new WP_HealthKit\Settings\Plugin_Settings();
});

// Business logic services
$wp_healthkit_container->singleton( 'audit-engine', function( $c ) {
    return new WP_HealthKit\Audit\Engine(
        $c->make( 'database' ),
        $c->make( 'cache' ),
        $c->make( 'logger' )
    );
});

$wp_healthkit_container->singleton( 'report-generator', function( $c ) {
    return new WP_HealthKit\Reports\Generator(
        $c->make( 'audit-engine' ),
        $c->make( 'logger' )
    );
});

// Admin interface
$wp_healthkit_container->singleton( 'admin-pages', function( $c ) {
    return new WP_HealthKit\Admin\Pages(
        $c->make( 'settings' ),
        $c->make( 'report-generator' ),
        $c->make( 'logger' )
    );
});

// Main plugin class
$wp_healthkit_container->singleton( 'plugin', function( $c ) {
    return new WP_HealthKit\Plugin(
        $c->make( 'audit-engine' ),
        $c->make( 'admin-pages' ),
        $c->make( 'logger' )
    );
});

Using the Container in Hooks:

// Instead of:
add_action( 'save_post', function() {
    global $wpdb;
    $audit = new Audit_Engine( $wpdb );
    $audit->scan_post( get_the_ID() );
});

// Do this:
add_action( 'save_post', function( $post_id ) {
    global $wp_healthkit_container;
    $audit = $wp_healthkit_container->make( 'audit-engine' );
    $audit->scan_post( $post_id );
});

Mid-Article CTA

Is your WordPress plugin using modern architectural patterns? WP HealthKit scans your plugin code to detect tight coupling, missing dependency injection, and global state issues. Upload your plugin to WP HealthKit to receive architecture analysis and recommendations for improving maintainability.


Testing with Dependency Injection

The primary benefit of DI becomes apparent when testing. With dependencies injected, you can provide mock implementations for testing.

Unit Testing with Mock Dependencies:

<?php

use PHPUnit\Framework\TestCase;

class UserManager_Test extends TestCase {
    
    /**
     * Test that get_user returns cached data when available
     */
    public function test_get_user_returns_cached_data() {
        // Create mock cache
        $cache = $this->createMock( Cache::class );
        $cache->expects( $this->once() )
            ->method( 'get' )
            ->with( 'user_1' )
            ->willReturn( (object) array( 'ID' => 1, 'user_login' => 'admin' ) );
        
        // Create mock database (won't be called because cache hits)
        $database = $this->createMock( Database::class );
        $database->expects( $this->never() )
            ->method( 'query' );
        
        // Create mock logger
        $logger = $this->createMock( Logger::class );
        
        // Create manager with mocks
        $manager = new UserManager( $database, $cache, $logger );
        
        // Test
        $user = $manager->get_user( 1 );
        $this->assertEquals( 1, $user->ID );
        $this->assertEquals( 'admin', $user->user_login );
    }
    
    /**
     * Test that get_user queries database when cache misses
     */
    public function test_get_user_queries_database_on_cache_miss() {
        // Create mock cache that returns nothing
        $cache = $this->createMock( Cache::class );
        $cache->expects( $this->once() )
            ->method( 'get' )
            ->with( 'user_1' )
            ->willReturn( null );
        
        // Create mock database that returns a user
        $user_object = (object) array( 'ID' => 1, 'user_login' => 'admin' );
        $database = $this->createMock( Database::class );
        $database->expects( $this->once() )
            ->method( 'query' )
            ->willReturn( $user_object );
        
        // Expect cache to be set after database query
        $cache->expects( $this->once() )
            ->method( 'set' )
            ->with( 'user_1', $user_object );
        
        $logger = $this->createMock( Logger::class );
        
        $manager = new UserManager( $database, $cache, $logger );
        
        // Test
        $user = $manager->get_user( 1 );
        $this->assertEquals( 1, $user->ID );
    }
}

Integration Testing with Test Container:

class UserManager_Integration_Test extends TestCase {
    
    protected $container;
    
    public function setUp() {
        // Create container with test implementations
        $this->container = new Test_Container();
        
        // Register test database (SQLite in-memory)
        $this->container->singleton( 'database', function() {
            return new SQLite_Test_Database( ':memory:' );
        });
        
        // Register test cache (no persistence)
        $this->container->singleton( 'cache', function() {
            return new Memory_Cache();
        });
        
        // Register test logger
        $this->container->singleton( 'logger', function() {
            return new Null_Logger(); // Doesn't output anything
        });
    }
    
    public function test_user_manager_full_workflow() {
        // Get manager from test container
        $manager = new UserManager(
            $this->container->make( 'database' ),
            $this->container->make( 'cache' ),
            $this->container->make( 'logger' )
        );
        
        // Insert test user
        $this->container->make( 'database' )->insert(
            'users',
            array( 'ID' => 1, 'user_login' => 'testuser' )
        );
        
        // Test retrieval
        $user = $manager->get_user( 1 );
        $this->assertNotNull( $user );
        $this->assertEquals( 'testuser', $user->user_login );
    }
}

Avoiding Global State

Global state—the use of global variables, singletons, and static methods—makes code hard to test and understand. Dependency injection eliminates most global state.

Before: Heavy Global State

// Anti-pattern: Global state everywhere
class BlogPost {
    public static function get( $post_id ) {
        global $wpdb;
        
        // Coupled to global $wpdb
        return $wpdb->get_row(
            "SELECT * FROM {$wpdb->posts} WHERE ID = $post_id"
        );
    }
    
    public static function save( $post ) {
        global $wpdb;
        
        // More global state
        $wpdb->update( $wpdb->posts, $post );
    }
}

// Usage - hidden dependencies
$post = BlogPost::get( 1 );
$post->post_title = 'New Title';
BlogPost::save( $post );

After: No Global State

// Better: All dependencies explicit
class BlogPost {
    private $database;
    
    public function __construct( Database $database ) {
        $this->database = $database;
    }
    
    public function get( $post_id ) {
        // Dependency is explicit
        return $this->database->get( 'posts', $post_id );
    }
    
    public function save( $post ) {
        // Clear what's happening
        return $this->database->update( 'posts', $post );
    }
}

// Usage - dependencies are clear
$post_service = new BlogPost( $database );
$post = $post_service->get( 1 );
$post->post_title = 'New Title';
$post_service->save( $post );

The difference is subtle but profound. With DI, anyone reading the code immediately knows what dependencies the class needs. With globals, you have to search the code to understand what gets accessed.

Practical Refactoring Examples

Here are real WordPress plugin patterns and how to refactor them with DI.

Example 1: Refactoring a Settings Class

// BEFORE: Direct WordPress API calls
class Plugin_Settings {
    public function get( $option ) {
        return get_option( 'my_plugin_' . $option );
    }
    
    public function set( $option, $value ) {
        return update_option( 'my_plugin_' . $option, $value );
    }
    
    public function delete( $option ) {
        return delete_option( 'my_plugin_' . $option );
    }
}

// Usage
$settings = new Plugin_Settings();
$api_key = $settings->get( 'api_key' );

// AFTER: Injected option storage
interface OptionStore {
    public function get( $key );
    public function set( $key, $value );
    public function delete( $key );
}

class WordPress_OptionStore implements OptionStore {
    private $prefix;
    
    public function __construct( $prefix = 'my_plugin_' ) {
        $this->prefix = $prefix;
    }
    
    public function get( $key ) {
        return get_option( $this->prefix . $key );
    }
    
    public function set( $key, $value ) {
        return update_option( $this->prefix . $key, $value );
    }
    
    public function delete( $key ) {
        return delete_option( $this->prefix . $key );
    }
}

class Plugin_Settings {
    private $option_store;
    
    public function __construct( OptionStore $option_store ) {
        $this->option_store = $option_store;
    }
    
    public function get( $option ) {
        return $this->option_store->get( $option );
    }
    
    public function set( $option, $value ) {
        return $this->option_store->set( $option, $value );
    }
    
    public function delete( $option ) {
        return $this->option_store->delete( $option );
    }
}

// Test with mock storage
class Memory_OptionStore implements OptionStore {
    private $options = array();
    
    public function get( $key ) {
        return isset( $this->options[$key] ) ? $this->options[$key] : null;
    }
    
    public function set( $key, $value ) {
        $this->options[$key] = $value;
        return true;
    }
    
    public function delete( $key ) {
        unset( $this->options[$key] );
        return true;
    }
}

// Usage
$store = new WordPress_OptionStore();
$settings = new Plugin_Settings( $store );
$api_key = $settings->get( 'api_key' );

// Testing
$test_store = new Memory_OptionStore();
$test_settings = new Plugin_Settings( $test_store );
// No database access during tests

Example 2: Refactoring Admin Pages

// BEFORE: Tightly coupled to WordPress
class Settings_Page {
    public function render() {
        check_admin_referer( 'my_plugin_settings' );
        
        if ( isset( $_POST['submit'] ) ) {
            update_option( 'my_api_key', $_POST['api_key'] );
            echo '<div class="updated"><p>Settings saved!</p></div>';
        }
        
        $api_key = get_option( 'my_api_key' );
        
        echo '<div class="wrap">';
        echo '<h1>Settings</h1>';
        echo '<form method="post">';
        wp_nonce_field( 'my_plugin_settings' );
        echo '<input type="text" name="api_key" value="' . esc_attr( $api_key ) . '" />';
        echo '<input type="submit" name="submit" value="Save" />';
        echo '</form>';
        echo '</div>';
    }
}

// AFTER: Separated concerns with DI
interface SettingsUI {
    public function render( $data );
}

class HTML_SettingsUI implements SettingsUI {
    public function render( $data ) {
        echo '<div class="wrap">';
        echo '<h1>Settings</h1>';
        echo $data['content'];
        echo '</div>';
    }
}

class Settings_Page {
    private $settings;
    private $ui;
    
    public function __construct( Plugin_Settings $settings, SettingsUI $ui ) {
        $this->settings = $settings;
        $this->ui = $ui;
    }
    
    public function handle_request() {
        $data = array( 'success' => false );
        
        if ( isset( $_POST['submit'] ) ) {
            check_admin_referer( 'my_plugin_settings' );
            
            $api_key = sanitize_text_field( $_POST['api_key'] );
            $this->settings->set( 'api_key', $api_key );
            
            $data['success'] = true;
        }
        
        $data['api_key'] = $this->settings->get( 'api_key' );
        return $data;
    }
    
    public function render() {
        $data = $this->handle_request();
        
        $content = '';
        if ( $data['success'] ) {
            $content .= '<div class="updated"><p>Settings saved!</p></div>';
        }
        
        $content .= '<form method="post">';
        $content .= wp_nonce_field( 'my_plugin_settings', '', true, false );
        $content .= '<input type="text" name="api_key" value="' . esc_attr( $data['api_key'] ) . '" />';
        $content .= '<input type="submit" name="submit" value="Save" />';
        $content .= '</form>';
        
        $data['content'] = $content;
        $this->ui->render( $data );
    }
}

// Test UI
class Test_UI implements SettingsUI {
    public $last_data;
    
    public function render( $data ) {
        $this->last_data = $data;
    }
}

// Testing
$test_ui = new Test_UI();
$page = new Settings_Page( $test_settings, $test_ui );
$page->render();

// Verify rendering without outputting HTML
$this->assertNotNull( $test_ui->last_data['content'] );

Container Patterns and Best Practices

Use Type Hints for Clarity:

// Good: Clear dependencies
public function __construct( Database $database, Logger $logger ) {
    $this->database = $database;
    $this->logger = $logger;
}

// Avoid: Ambiguous dependencies
public function __construct( $database, $logger ) {
    $this->database = $database;
    $this->logger = $logger;
}

Lazy Loading for Performance:

class WP_HealthKit_Container {
    public function get( $name ) {
        if ( ! isset( $this->instances[$name] ) ) {
            // Only instantiate when requested
            $this->instances[$name] = $this->make( $name );
        }
        
        return $this->instances[$name];
    }
}

Auto-wiring Dependencies:

class Smart_Container extends WP_HealthKit_Container {
    public function auto_wire( $class_name ) {
        $reflection = new ReflectionClass( $class_name );
        $constructor = $reflection->getConstructor();
        
        if ( ! $constructor ) {
            return new $class_name();
        }
        
        $params = array();
        foreach ( $constructor->getParameters() as $param ) {
            $param_class = $param->getClass();
            
            if ( $param_class ) {
                $params[] = $this->make( $param_class->getName() );
            }
        }
        
        return $reflection->newInstanceArgs( $params );
    }
}

Additional Resources

For a comprehensive view of how WP HealthKit approaches plugin analysis, explore our 17 verification layers or browse the plugin directory to see real audit scores. Ready to check your own plugin? Run a free audit now.

Frequently Asked Questions

Doesn't DI make code more complicated?

Initially, yes. But the complexity is upfront design that pays dividends in testing, maintenance, and flexibility. A small plugin might not benefit, but any plugin with multiple classes and dependencies becomes simpler with DI.

Should I use a third-party DI container?

The WordPress Plugin Handbook recommends building your own lightweight container rather than adding external dependencies. PHP-FIG's Container Interface (PSR-11) provides standards if you want to use community containers like PHP-DI or Pimple.

How do I handle circular dependencies?

Circular dependencies indicate architectural problems. If class A depends on B, and B depends on A, refactor to extract the shared functionality into a third class. True circular dependencies are symptoms of poor design.

Can I use DI with WordPress hooks?

Yes. The container manages object instantiation. You register hook callbacks that use the container to get dependencies. Just don't create circular dependencies through hooks.

What's the difference between Service Locator and Dependency Injection?

Service Locator is when objects request dependencies from a central service (the locator). This is often used in WordPress but is actually an anti-pattern because it hides dependencies. Dependency Injection is better because dependencies are explicit.

Should every class use constructor injection?

No. Simple utility classes that don't have dependencies don't need it. Apply DI where it provides value: classes with multiple dependencies, classes that are tested, classes with replaceable implementations.

Conclusion

Dependency injection transforms WordPress plugin architecture from tightly coupled procedural code to testable, maintainable object-oriented code. Constructor injection, service containers, and proper interface design make plugins flexible and robust.

The implementation patterns here—lightweight containers, singleton management, WordPress hook integration, and comprehensive testing—are production-ready approaches used in modern WordPress projects.

However, implementing DI correctly requires understanding architecture principles beyond just syntax. Code written without considering testability often can't be refactored to use DI without major rewrites.

WP HealthKit automatically analyzes your plugin's architecture, detects tight coupling and missing DI patterns, and provides recommendations for improving maintainability.

Upload your plugin to WP HealthKit to receive a detailed architecture analysis including recommendations for implementing dependency injection, reducing global state, and improving testability.

Ready to audit your plugin?

WP HealthKit checks for all the issues in this article and 40+ more across 49 verification layers.

Comments

WordPress Plugin Dependency Injection: PHP Design Patterns | WP HealthKit