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.
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:
PHPfunction 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.
PHPnamespace 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.
PHPnamespace 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
- Refactor: Create a
Databaseclass in your Knowledge Base plugin that wraps$wpdb. - Decouple: Create a
Settingsservice. - Inject: Create an
AdminPageclass that requires bothDatabaseandSettingsin its constructor. - 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.
Work with me

Custom WordPress Plugin Development
Custom WordPress & WooCommerce plugins built to standard — by the developer behind a plugin with 5,000+ active installs and a SaaS with 10,000+ users.

Custom WordPress Theme Development
A custom WordPress theme built exactly to your design — fast, clean, and easy to manage. No bloated page builders, no compromises.