PHP

A Concrete Guide to Dependency Injection

As we develop concrete5, we are very conscious about keeping the code base modern, so that we don't have to go through a massive, painful rewrite like we did with version 5.7. This means that we're frequently deep in the weeds, applying new concepts and functionality to new and old code alike. With such a deep focus on code, it's easy to lose sight that many talented developers have no familiarity with terminology or concepts that we take for granted. With that in mind, I'm going to come up for air and explain some concepts that come up frequently when working in advanced concrete5 development.

Here, we're going to talk about Dependency Injection. There are numerous tutorials, blog posts and Stack Overflow questions, regarding Dependency Injection – but there's always room for some more.

While I might view some of these through the lens of concrete5, with examples that demonstrate concrete5 tooling, these concepts are applicable across all modern PHP frameworks, and even across programming languages.

The (Fine?) Old Days

If you've done any object-oriented development in PHP, you've probably seen something like this. Here's a class for listing boats on a website, using a database connection to retrieve them.

<?php

namespace Application\MarinaPortal\Boat;

class BoatLister
{

    public function getBoats()
    {
        $db = \Database::getConnection();
        $r = $db->executeQuery('select * from Boats');
        // Do interesting stuff here...
    }

}

My getBoats method is responsible for querying the database, retrieving a list of boats and turning them into objects, returning them to the user, etc... None of that really matters for what we're talking about: we're interested in just this line here:

$db = \Database::getConnection();

This code is example code: every framework will have a different way of retrieving the current active database connection. If you're a concrete5 developer who's been around since 5.6 or earlier, you've undoubtedly seen this countless times,

$db = Loader::db();

It all boils down to some class with a static method, responsible for giving a single copy of our object to any method that needs it.

This is a Dependency

This gets to the first term that we're interested in: dependency. So much of the confusion around dependency injection stems from its terminology, and the fact that such a lofty name was applied to something that has been around since the beginning of programming. This article says it better than I can: the term "Dependency Injection" is a "25-dollar term for a 5-cent concept."

Be that as it may. In our example above, our BoatLister class has a dependency. That means it depends upon our database connection class in order to run.

Problems

Let's be clear: our BoatLister::getBoats() code gets the job done. But it has problems.

Testability

This biggest problem with this code, and the reason why the PHP community and others have embraced Dependency Injection, is that it isn't easily testable. It isn't easily testable because my database connection variable isn't something I have access to via my class. It's closed off, hidden inside my method. Typically, when testing my class, I'd want to pass a mock/fake object that takes the place of a live connection object, and run my executeQuery code on that. I can't do that, because my dependency is created inside my getBoats() method. So instead, to test this method, I will have to either run a live copy of my database in the testing environment, or figure out some way to swap out the connection that my live Database::getConnection() method returns. The first is slow, the second potentially means brittle tests that end up breaking for unrelated reasons.

Much of concrete5's older core code isn't fully tested either – embracing Dependency Injection earlier likely would have made this less true, because a system built from the ground up with this approach in mind will be easier to test, making it less of a hassle to accomplish.

Duplicated Code

Testability isn't the only problem here. Imagine that our class has many methods. We're going to end up with some duplicated code.

public function getBoats()
{
    $db = \Database::getConnection();
    $r = $db->executeQuery('select * from Boats');
    // Do interesting stuff here...
}

public function getBoatByID($id)
{
    $db = \Database::getConnection();
    $r = $db->executeQuery('select * from Boats where id = ?', [$id]);
}

// etc...

How might we fix this? We could get the connection once, in the constructor, and just reference it in the methods:

public function __construct()
{
    $this->db = \Database::getConnection();
}

public function getBoats()
{
    $r = $this->db->executeQuery('select * from Boats');
    // Do interesting stuff here...
}

public function getBoatByID($id)
{
    $r = $this->db->executeQuery('select * from Boats where id = ?', [$id]);
}

We still have our testability problem, however. But as we migrate our code to move our single dependency into the constructor, we have unwittingly moved closer to a system that can start to employ dependency injection.

Move to Dependency Injection

Here's how this class's constructor looks after it employs Dependency Injection:

public function __construct(DatabaseConnection $db)
{
    $this->db = $db;
}

That's it! Your dependency has now been decoupled from the BoatLister class, and you're injecting it into your class, via the constructor. Now when testing this class you can create a mock database object in your code and pass it in, rather than having to work with a live database. (I wish I had known about this when writing the first tests for concrete5 eight or nine years ago.)

Why the Confusion?

So why so much confusion about Dependency Injection? I think it's a naming problem. There are two components to this process:

First, remove dependencies from your classes, setting up your classes so that the classes that they depend on are injected into them via the constructor or some setter method in the class. This is all Dependency Injection is, because you're now injecting your dependencies, rather than instantiating them from within the class!

A side effect of this, however, is that it puts a lot of work on a framework at runtime: every time you define a class, you need to pass the current dependencies into it. Additionally, if a dependency itself has dependencies, you might find yourself doing a lot of object instantiation, for a benefit that might seem obtuse. That's why frameworks that advocate for Dependency Injection frequently ship with a Dependency Injection Container, which is a secondary object that helps make it possible to easily register objects as dependencies, and have those classes automatically created with their dependencies. So instead of this

$list = new BoatLister(\Database::getConnection());

We would have this

$container = new Container();
$list = container->createObject('Application\MarinaPortal\Boat\BoatLister');

Our createObject method does some magic behind the scenes to automatically create the database connection object that the BoatLister class depends on.

This is why Dependency Injection is so confusing: the same term defines the simple process of externally providing instance variables to a class as it does to the Container object's magical auto-instantiation of object (which also "injects dependencies.")

More about Containers

There's more to learn here: each PHP framework provides its own Container implementation (concrete5's is based on Laravel's, FWIW) to Automated Dependency Injection using Containers.

I want to stress: Dependency Injection is not about magically creating objects and providing them to classes. That is the job of a Container. Dependency Injection is simply making your classes accept their dependencies externally. You can do that today, and make your code easier to read and to test – no framework required.

Loading Conversation