Dependency injection: PHP edition

aleksikauppila

Aleksi Kauppila

Posted on March 3, 2019

Dependency injection: PHP edition

"Dependency injection" is used in everyday programming jargon with two different meanings. A lot of the time it refers to "container technology". Other times it refers to the design pattern, which of course, is a source for confusion. Making this distinction to "DI the design pattern" and "DI the container" helps us understand the concept better.

The pattern

If i'd have to choose a very limited list of must-know design patterns, dependency injection would certainly be on that list. The pattern itself is incredibly simple and enables you to write more testable and configurable applications.

Wikipedia lists three types of dependency injections: constructor-, setter- and interface injection. We will be focusing on constructor injection as other types produce inferior design.

It's really this simple:

// Without dependency injection
class MyService
{
    private $myLogger;

    public function __construct()
    {
        $this->myLogger = new MyLogger();
    }
    //...
}

// With dependency injection
class MyService
{
    private $myLogger;

    public function __construct(MyLogger $myLogger)
    {
        $this->myLogger = $myLogger;
    }
    //...
}

Instead of creating the instance of MyLogger in MyService, it's created in client code and passed in via constructor. This is really all there is to dependency injection design pattern.

This allows us to unit test this class. We can stub, mock or fake the behaviour of MyLogger. This is especially important if the injected object uses filesystem, database or connects to an external service.

Already, with this small modification we've achieved better testability of MyService. To achieve differing behaviour at runtime and overall better maintainability, we would have to depend on an interface instead of a concrete class.

The container

The container is just an object that provides access to instances of our services. I actually really like the term "service container" as these containers usually work best when they build services. I don't recommend defining DTO's, DAO's, forms, value objects or others in the container.

The basic idea is simple:

  • You write instructions on how to create services. These are called service definitions.
  • You give these instructions to a Container or ContainerBuilder.
  • You ask the Container to provide an instance of some class: $container->get(MyService::class);.

There are also technologies that provide autowiring which will do some magic and allow you to have a lighter configuration. I don't have anything against autowiring, but i do prefer explicit service definitions.

I've used two vendors for dependency injection containers: symfony/dependency-injection and php-di/php-di. They can be introduced to your project even though you may use other framework, or even no framework at all. I won't go into details here how to setup these technologies as they've good documentations.

When it comes to using the container finally there's one VERY important gotcha: you shouldn't have access to the container in your application logic layer or any higher layers! The access should be limited to only on lower levels of the application where the components that run the application are built. In older versions of Symfony, there used to be a convention to summon services from container inside the controllers, but that's fortunately in the past now.

PHP-DI's "best practices" guide provides us with these important rules. I urge you to read the whole document as it contains helpful advice to make your life easier with containers.

  1. never get an entry from the container directly (always use dependency injection)
  2. more generally, write code decoupled from the container
  3. type-hint against interfaces, configure which implementation to use in the container's configuration

With the last point, we can continue where we left off with our simple example of dependency injection pattern.

To really make the code maintainable we have to adhere to dependency inversion principle which says:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

First, we make sure MyLogger implements Psr\Log\LoggerInterface. Then we use LoggerInterface as type-hint for our logger. If MyLogger is a third party dependency, you can refer to techniques presented here.

class MyService
{
    private $myLogger;

    public function __construct(LoggerInterface $logger)
    {
        $this->myLogger = $logger;
    }
    //...
}

After this change, our code is more testable, configurable and more robust against changes.

💖 💪 🙅 🚩
aleksikauppila
Aleksi Kauppila

Posted on March 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related