Generally what we want to do when we’re coding something is to call a function.
While some of the functions we call could stand alone and some could be methods on an object, they’re all just function calls.
So why does most PHP code have so many classes? I don’t think code organization is a good answer, since we have access to namespaces which can do that just as well. I believe the only valid reason apart from value objects is because of trying to manage dependencies.
Let’s say you are writing code which wants to refund a purchase through a billing system. You call something like refund_purchase($purchase)
and you expect to get back a receipt id for the refund. From most caller’s perspectives, that’s all we should need to worry about.
However, let’s say the refund function needs to do the following things:
- Contact the payment processor to initiate the refund.
- Create and save a new receipt to the database.
- Queue webhooks for the refund.
In order to do any of those things, it requires references to other systems. What if we want to switch out those other systems? Perhaps we want to test our function without a database, or we want to change the payment processor. This is where dependency injection comes into play.
Dependencies as Arguments
In order to use dependency injection in the simplest sense, we need to pass interfaces for each of the dependent systems into our function as arguments.
function refund_purchase($purchase, $processor, $database, $webhooks) {}
This is not scalable, though, because as the number of dependencies increases, so does the number of arguments, making it very confusing to call the function in the first place. In addition, when a developer wants to add a new refund call, they also have to figure out how to get each one of the dependencies, each of which may have their own dependencies!
Wrappers
The alternative to passing dependencies as arguments is to use a wrapper function to pre-inject our dependencies before they are needed. In PHP (and other Object-Oriented languages) the most accepted way to do this is using a class constructor as that wrapper. (In some other languages we might use a Higher Order Function, but this is much more challenging in PHP).
class Refunds {
public function __construct($processor, $database, $webhooks) {
$this->processor = $processor;
$this->database = $database;
$this->webhooks = $webhooks;
}
public function refund_purchase($purchase) {}
}
But now we are stuck with another problem: were do we call the wrapper? Since we might want to ask for a refund in any number of places, generally we must force the calling code to call the wrapper as well.
$wrapper = new Refunds($processor, $database, $webhooks);
$wrapper->refund_purchase($purchase);
That’s not a whole lot better than putting the dependencies into the function arguments, though; it’s confusing and annoying for the caller. Also, any time we want to add new dependencies we would have to change every single caller.
Factories
A solution to the issue of where to call the wrapper is to put it into a stand-alone function.
function create_refund() {
global $wpdb;
$processor = new DefaultProcessor();
$database = $wpdb;
$webhooks = new Webhooks();
return new Refunds($processor, $database, $webhooks);
}
The calling code must still create the wrapper before it uses the function it wants, but at least that can be broken down to a one-liner.
create_refund()->refund_purchase($purchase);
In practice, these wrapper calls are often put into static functions, and can even be functions attached to the classes they are meant to wrap, which puts them close to the function call we want and so makes them easier to discover.
Forwarding Functions
It’s also possible to wrap the wrapper call into its own forwarding function to remove any need for the caller to know about our wrappers.
function refund_purchase_with_defaults($purchase) {
return create_refund()->refund_purchase($purchase);
}
This may seem like an extreme level of redirection, but it provides the best of both worlds: as long as the forwarding function does not do anything apart from call the wrapper and its method, most callers can ignore dependency injection entirely. This only works if we make sure never to use the forwarding functions inside other wrappers, though.
Containers
Eventually we end up with tons of wrappers, as there are many functions that have dependencies and they can’t always be grouped easily. This presents two new problems: it can be hard to locate the wrapper you need, and in order to keep everything as simple as possible for the caller, we end up creating and re-creating the wrapper class many times within a single execution of the code. This can immensely bloat our memory usage since each one of those instances keeps its own state and references.
Therefore many projects end up with a Dependency Injection Container which puts all the wrapper functions into one place and also caches the instances it creates, making it very easy for callers to keep their one-liners without having to worry about memory issues.
class Container {
public function create_refund() {
global $wpdb;
if (! $this->refunds) {
$processor = new DefaultProcessor();
$database = $wpdb;
$webhooks = new Webhooks();
$this->refunds = new Refunds($processor, $database, $webhooks);
}
return $this->refunds;
}
}
Having these wrappers creates a new problem for our functions themselves, though, which I mentioned above: it’s now so easy to call other wrapped functions that it’s tempting to use the wrappers to make calls to other systems rather than using injected references. Doing this defeats the purpose of our wrappers entirely and we end up right back where we started. Therefore we must have some mechanism, usually just code review, which prevents anyone from using wrappers within a wrapped function.
Do we need all this?
All of this work is built on the premise that dependency injection is good and a necessary thing to have. The counter-argument is that without that premise, our code is so much simpler. So let’s examine the concept a bit more.
In any given project there will probably be functions for which the dependencies are relatively trivial. Let’s say a function needs to log events for analysis, but we don’t want to log anything while running automated tests. Rather than injecting a logger, we could just have the logger function itself detect if we’re running inside a test and if so, do nothing. If more special cases are needed, those can be added directly to the function as well.
function log_message($message) {
if (! defined('ARE_TESTS_RUNNING')) {
write_log_message($message);
}
}
This technique has the advantage that it’s more foolproof than relying on callers to substitute dependencies, so we might be doing it anyway to protect our data. Even though it relies on global variables and constants, a PHP process is bounded in both time and scope so in many projects this reliance on globals creates fewer problems than it solves.
It’s also likely that in any given project there are at least a few functions which have very complex dependencies, and therefore are hard to test without running them in isolation. These functions probably benefit from using a wrapper approach as described above.
So when exactly do we need dependency injection and when is it just needless work? Even though to me the elegance of separating dependencies is a reason in itself, to be honest I can only think of two real needs. I think that we must ask these questions of each dependency we find.
Questions to ask
- Is a dependency large or complex enough to require mocking during testing, or do we need to test the interaction with a dependency itself?
- Is it likely that a dependency will need to be changed now or in the future because it represents some system that has more than one implementation?
As soon as any dependency in a project answers “yes” to one of the above questions, I think that it’s worth including a Dependency Injection system of some kind. Even if the answers are “no”, it might be a good idea if the project is a library which itself is used by other systems, because it’s often hard to predict how a library might be used. It can be a pain to be forced by your libraries to adopt specific other libraries.
And if you do have a Dependency Injection system, if any given dependency does not answer “yes” to the above questions, then perhaps you can ignore injecting it. The purist in me rebels against the idea of a single function including both injected dependencies and non-injected dependencies, but given the large amount of code needed to use a wrapper in PHP, it may just be the better choice.
(Photo by John Carlisle on Unsplash)