Back to Blog
Lesson 2 of the Advanced WordPress Plugin Engineering: Scale, Security & React UIs course
WordPressJune 26, 20263 min read

Dependency Injection Basics for Scalable WordPress Plugins

Learn how to implement Dependency Injection and a service container in WordPress to eliminate global state and build modular, testable plugins.

WordPressPHPDependency InjectionDesign PatternsArchitectureRefactoringplugin-development

Previously in this course, we established the foundation for modern development by configuring PSR-4 autoloading and Composer. With those standards in place, we can now move away from the "global variable" trap that plagues most WordPress plugins.

In this lesson, we address the architectural debt created by tight coupling. By implementing Dependency Injection (DI) and a Service Container, we will transform your plugin into a collection of loosely coupled, swappable components.

The Problem: Global State and Tight Coupling

In procedural WordPress development, we often see code like this:

PHP
function get_kb_data() {
    global $wpdb; #6A9955">// Hard dependency
    return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}kb_articles");
}

This code is difficult to test because it's impossible to swap $wpdb for a mock database object. It’s also hard to maintain; if your plugin’s logic grows, you’ll find yourself passing global variables across dozens of functions.

Dependency Injection (DI) solves this by requiring classes to receive their dependencies (via constructor or setter) rather than creating them or fetching them from the global scope. Design Patterns like the Service Container act as the central "factory" that manages the instantiation of these objects.

Implementing a Service Container

A Service Container is simply an associative array that holds "recipes" for creating your objects. When you need an object, you ask the container to resolve it.

Let's build a minimalist container for our Knowledge Base plugin.

PHP
namespace KnowledgeBase\Core;

class Container {
    protected $bindings = [];
    protected $instances = [];

    #6A9955">// Register a recipe
    public function bind($abstract, callable $concrete) {
        $this->bindings[$abstract] = $concrete;
    }

    #6A9955">// Resolve an instance
    public function get($abstract) {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        $concrete = $this->bindings[$abstract];
        $this->instances[$abstract] = $concrete($this);
        return $this->instances[$abstract];
    }
}

Constructor Injection in Practice

Now, let's apply this to our plugin. Suppose we have a Logger class and an ArticleManager class. Instead of ArticleManager creating a Logger internally, we inject it.

PHP
namespace KnowledgeBase\Services;

class Logger {
    public function log($message) { error_log($message); }
}

class ArticleManager {
    protected $logger;

    #6A9955">// Constructor Injection
    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }

    public function publish($id) {
        $this->logger->log("Published article: $id");
    }
}

By injecting the Logger, we can easily pass a "NullLogger" or "MockLogger" during unit testing without changing the ArticleManager code. This is the essence of decoupling.

Orchestrating with the Container

In your main plugin file, you register your services during the initialization phase:

PHP
$container = new \KnowledgeBase\Core\Container();

$container->bind('Logger', function() {
    return new \KnowledgeBase\Services\Logger();
});

$container->bind('ArticleManager', function($c) {
    return new \KnowledgeBase\Services\ArticleManager($c->get('Logger'));
});

#6A9955">// Use it
$manager = $container->get('ArticleManager');
$manager->publish(123);

Hands-on Exercise

  1. Refactor: Create a Database class in your Knowledge Base plugin that wraps $wpdb.
  2. Decouple: Create a Settings service.
  3. Inject: Create an AdminPage class that requires both Database and Settings in its constructor.
  4. Resolve: Use a container to instantiate the AdminPage, ensuring that the dependencies are passed correctly at runtime.

Common Pitfalls

  • Service Locator Anti-pattern: Passing the entire container into a class is a common mistake. Only inject the specific dependencies a class needs.
  • Circular Dependencies: If Class A needs Class B, and Class B needs Class A, your container will trigger a stack overflow. Keep your dependency graph directed and acyclic.
  • Over-engineering: Don't build a container for a tiny plugin. DI is a tool for scale; use it when you have more than 5-10 interconnected services.

If you have worked with Laravel, you might recognize these patterns. Concepts like Laravel service container binding and Advanced Dependency Injection with Laravel Service Providers are foundational to this approach, even within the WordPress ecosystem.

Recap

Dependency Injection moves us from "pulling" global dependencies to "pushing" them into our classes. By using a Service Container, we define the structure of our application in one place, making our code modular, testable, and significantly easier to debug as the Knowledge Base plugin grows.

Up next: Architecting Service Providers — we will learn how to move our container registrations into dedicated classes to keep our main plugin file clean and organized.

Similar Posts