I got a call to take over a project with a big custom events plugin, and it was a mess. The previous developers had built everything with classes, which is a good start, but the classes were all tangled together. The main ticketing class created a new email handler, which in turn created a new logging utility. You couldn’t change one thing without breaking five others, and testing any of it in isolation was hopeless. That is what happens without WordPress dependency injection: the code fights you the whole way.
When classes create the objects they depend on directly (for example, $logger = new MyLogger();), they become tightly coupled. You can’t use the ticketing class without also dragging in that specific email handler and that specific logger. If you wanted a different logging service for certain events, you couldn’t switch without rewriting the whole thing.
The obvious fix that isn’t enough
My first thought was to refactor the constructors. Instead of classes creating their own dependencies, I’d pass them in, so the ticketing class constructor became public function __construct( $email_handler, $logger ). And yeah, that worked, for a minute. It decoupled the classes from each other, but it pushed the problem somewhere else. Now the main plugin file was responsible for creating every object in the exact right order, one big fragile block of initialization code. It was better, but not a real fix.
A simple, practical DI container
You don’t need a big framework to solve this. You need one central place that manages how your objects get built: a simple Dependency Injection (DI) container. The idea, which builds on a concept I saw over at carlalexander.ca, is a class that holds recipes for creating your objects. When you need an object, you ask the container for it and it builds it with all the right dependencies.
We can build a lightweight container that uses anonymous functions (closures) to define these “recipes.” It’s simpler than it sounds, and it makes the code much easier to rearrange.
<?php
class MyPlugin_Container implements ArrayAccess {
private $values = [];
// Method to define a "service" - an object that should only be created once.
public function service(Closure $closure) {
return function ($c) use ($closure) {
static $object;
if (null === $object) {
$object = $closure($c);
}
return $object;
};
}
// ArrayAccess methods to make the container behave like an array.
public function offsetSet($key, $value) {
$this->values[$key] = $value;
}
public function offsetGet($key) {
if (!isset($this->values[$key])) {
throw new InvalidArgumentException("Value '{$key}' not found.");
}
$value = $this->values[$key];
// If it's a function, run it to create the object.
// Pass the container itself so it can resolve other dependencies.
return $value instanceof Closure ? $value($this) : $value;
}
public function offsetExists($key) {
return isset($this->values[$key]);
}
public function offsetUnset($key) {
unset($this->values[$key]);
}
}
// --- How you use it ---
$container = new MyPlugin_Container();
// Define the logger as a service (it will only be created once).
$container['logger'] = $container->service(function() {
return new My_File_Logger('/path/to/logs.txt');
});
// Define the email handler, which depends on the logger.
$container['email_handler'] = function($c) {
return new My_Email_Handler($c['logger']);
};
// Now, get the email handler. The container builds it with the logger automatically.
$email_handler = $container['email_handler'];
Why this is worth it
This changes how you build plugins. Instead of a tangled web of dependencies, you get a central, organized way to manage your objects. A few concrete wins:
- Swapping the file logger for a database logger is a one-line change in the container setup, and the rest of the code stays untouched.
- You can test classes in isolation by passing mock objects as their dependencies, which makes reliable tests much easier to write.
- A new developer can read the container setup and see how the plugin is wired together, instead of digging through dozens of files.
This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.
Moving away from singletons and direct instantiation is a real step up for WordPress code that has to grow over time. A simple DI container gives you a lot of leverage for very little setup.