Using DIC the right way

DIC stands for Dependency Injection Container, which is a tool that manages the construction and wiring up of application services. It closely relates to the letter "D" of a SOLID acronym - Dependency Inversion Principle and is employed to facilitate adhering to the principle.

By their nature, DI Containers are also Service Locator implementations, design pattern that is the exact opposite to Dependency Injection. Because of that, DI Container is a double-edged sword which can mislead you if not used wisely, and ironically bring your code into a state in which there is no dependency injection at all.

Core vs Assembly code

The easiest way to comprehend what is right and what is not with regard to using DI Containers is by looking at the code we write in the form of two extremely simplified and broad categories.

Most of today's application share similar types of code ingredients. Business logic is made up of entities, repositories, services. Depending on the interfaces application exposes and patterns used for these purposes, we find middleware, action controllers and console commands. Often there is a need for writing customizations and add-ons for lower level components such as database handlers, loggers, and similar. All this belongs to the first, dominant category that I'm gonna formulate as the core.

All these classes and components do not function by themselves. At some point you need to create and connect their real instances, and run them. Configuration, factory classes, construction logic, bootstrapping scripts, and application runner itself constitute our second category - a thin layer that assembles the various components of our system and eventually runs it.

The following diagram illustrates this division:

Core vs Assembly layer

It is understood that DI Container has its place only in the outer layer, but sometimes it still manages to reach the core. This is precisely the problem that I want to point out and raise awareness of.

DIC must not leak into the core

Ideally, the only place in the system where you refer the DI Container is that outermost part of your application, consisting of factories, configurations and bootstrapping scripts, meaning that the core stuff must not be aware of the existence of a DI Container. This may sound too idealistic, but it's actually very feasible.

The phenomenon of a DIC's penetration into the core actually leads to the adoption of the aforementioned Service Locator design pattern. Many consider it anti-pattern for a reason. I see two major problems with doing service location within core classes, either in case of injecting service locator as a dependency, or worse, randomly pulling dependencies from a global/static service locator:

The two strongest arguments against this pattern that I find important are:

  • hidden dependencies - unlike a class that requires explicit dependencies via constructor injection, thus clearly shouting how it's supposed to be used, class that has a service locator as a dependency forces consumer to examine the code for calls to the locator.
  • testing difficulties - having a DIC at the class entrance not only unnecessarily complicates injecting test doubles for real dependencies, but also causes DIC leakage into test code.

Establish the assembly layer

Keeping the DIC under control is all about creating a clear boundary between the core and assembly layer and strictly adhering to a continuous process of:

  1. writing factory classes for core services
  2. registering services and corresponding factories to the DI Container

Having a boundary between the core and assembly code is all about separating concerns, they don't have to be separated at the filesystem level at all. That being said, there's nothing wrong with factories sitting right next to the classes for whose creation they are responsible:

src/
    Framework/
        Db/
            DbConnectionFactory.php
            DbConnectionInterface.php
            DoctrineDbConnection.php
    Post/
        Post.php
        PostRepositoryFactory.php
        PostRepositoryInterface.php
        SqlPostRepository.php

Typically, factory is a callable or a class implementing __invoke() method that gets container instance as an argument:

class PostRepositoryFactory
{
    public function __invoke(ContainerInterface $container) : PostRepositoryInterface
    {
        return new SqlPostRepository($container->get(DbConnectionInterface::class));
    }
}

The vast majority of popular DI Containers for PHP complies with common PSR-11 interface, which is a good news from the standpoint of interoperability, as you will be able to easily switch to some alternative DI Container without touching factories.

Depending on the DI Container of your choice, you will either initialize it (fill it with service definitions) programmatically or via configuration. I prefer the latter approach because I find it much more convenient and easier to maintain:

config/services.global.php

return [
    'di' => [
        'factories' => [
            MyApp\Framework\Db\DbConnectionInterface::class => MyApp\Framework\Db\DbConnectionFactory::class,
            MyApp\Post\PostRepositoryInterface::class => MyApp\Post\PostRepositoryFactory::class,
        ],
    ],
];

Initializing DI Container means loading/merging configuration and feeding container with the data from a relevant configuration key, which is di in this case. I'm gonna use Zend Service Manager as an example:

src/bootstrap.php

$config = [];

$files = glob('config/{{,*.}global,{,*.}local}.php', GLOB_BRACE);

foreach ($files as $file) {
    $config = array_merge($config, include $file);
}

$config = new ArrayObject($config, ArrayObject::ARRAY_AS_PROPS);

$diContainer = new Zend\ServiceManager\ServiceManager($config['di']);
$diContainer->set('config', $config);

return $diContainer;

Everything from general purpose services (database adapter, mailer, logger, and similar), over domain services, repositories, and even application runners should be set in a DI container. That way, your application's main entry point will look as simple as:

public/index.php

/** @var \Psr\Container\ContainerInterface $container */
$container = require __DIR__ . '/../src/bootstrap.php';

$container->get(Application::class)->run();

Controllers are no exception

Controllers are probably the most commonly misinterpreted elements of MVC-like PHP applications. With the arrival of new concepts and patterns, such tradition is transferred to action handlers and middleware. The most famous violation is the one for which they get the status of "fat", which is due to putting too much business logic in a class of such high level of abstraction.

With regard to the topic of this article, they are misused by making them DIC-aware. Just like any other service class, controller can have explicit dependencies required through their constructor, so in my mind there's no reason for them to be treated differently. You register them into dependency injection container just like any other service, meaning that depending on the specific DIC you use, you'll either write factories or rely on some "magic" mechanism for automatic injection of dependencies.

If you're worried that this approach might result in too many parameters in controller's constructor, that is a code smell and clear indicator that you should probably split it into few, smaller, cohesive controller classes.

Efficient DI configurations

As you define more and more services, especially if you follow my advice on controllers, your DI configuration can grow to the point if being bulky and difficult to maintain. Good practice of splitting big classes into smaller ones is a valid point in this case. Simply divide your single service definitions file into smaller logical units:

config/services.global.php

return [
    'di' => [
        'factories' => [
            MyApp\Framework\Db\DbConnectionInterface::class => MyApp\Framework\Db\DbConnectionFactory::class,
            MyApp\Post\PostRepositoryInterface::class => MyApp\Post\PostRepositoryFactory::class,
        ],
    ],
];

config/web.global.php

return [
    'di' => [
        'factories' => [
            MyApp\Post\Web\SubmitPostAction::class => MyApp\Post\Web\SubmitPostActionFactory::class,
            MyApp\Post\Web\ViewPostAction::class => MyApp\Post\Web\ViewPostActionFactory::class,
        ],
    ],
    'templates' => [
        'post' => 'resources/templates/post',
    ],
];

Doing so, not only that you will be able to organize services configuration more efficiently, but also other configuration option that relate to them.

Auto-magic wiring of dependencies

If you find writing factories and registering services tiring and tedious activity, look for some Dependency Injection tool that reduces that effort, either through more efficient configuration-driven approach or some "magic" mechanism for automatic injection of dependencies based on type-hints. I don't believe in magic, and I prefer to keep things under control by having explicit service definitions. Zend Service Manager is my weapon of choice in this case, which can further simplify DI configuration through its Config Abstract Factory feature.

Final thoughts

Dependency Injection is the crucial concept for building maintainable applications, and DI Containers facilitate this idea. If not used the right way, DIC can run wild and get out of control, so you should know how to tame it.

The key lesson is that DI Container should be used as Dependency Injection system, and not as a Service Locator system. Being consistent with this conviction means a discipline of keeping core services completely ignorant of their factories and the overall assembly/runtime process they are involved in. To me, Service Locator is an anti-pattern if used within the core code context. As we have seen, it is completely valid and inevitable to use it within factories and application runner scripts for example.

Ultimately, Dependency Injection Container is a tool, while Dependency Injection itself is a principle, and software design principles are evergreen. Let the principles dominate your code, and use the tools in an unobtrusive way.


dependency-injection container service-locator