Dependency Injection Container configuration is, in reality, a Code

When using the dependency injection container there is a need for a lot of configuration. This article proposes a practice of treating this configuration as a code and organize it (separate it) accordingly its domain.

en 17 Aug 2019
Reading time: 2 minutes
PHP

In this article, I will use the Nette Dependency Injection Container as an example, but this practice is applicable in general.

Problem

The services.neon configuration file usually looks like this:

services:
    - \Blog\Article\ArticleRepository
    - \Shop\Order\OrderFactory
    # ... and (maybe thousands?) more lines

And this has couple issues:

  • Configurations files are growing and growing and it became hard to maintain them.
  • Domains (in this case Blog and Shop) are usually getting mixed and dependencies are not clear.

Discussion

Dependency Injection Container configuration is not a "real" configuration. The stuff that is written there is the very top-level of the application. It describes how all the other parts of the code should be bundled together. The so-called "configuration" is, in fact, the executable code interpreted by the Dependency Injection Container engine.

Proposed solution

This article propose splitting the services.neon file into many small files and placing them near the code that they describe to solve both problems.

In the context of our example the file structure will look like this:

src
├── Blog
|   └── Article
|       ├── ArticleRepository.php   
|       └── services.neon # contains services from Blog/Article namespace
├── Shop
|   └── Order
|       ├── OrderFactory.php   
|       └── services.neon # contains services from Shop/Order namespace        
|
└── services.neon # contains 3rd party services

Each services.neon file than contains the only configuration for services from its namespace. This practice will result in small well-arranged files that can be easily maintained and also organized according to the domain.

Third-party dependencies should be registered in the top-level services.neon file in the root of the application.

Technical details

This pattern is used in the source code of this blog and its code can be used as living example of this practice.

Because of a lot of small neon files scattered through the project we need some smart loading. In this example, we use file finder from nette/utils package to get a list of all services.neon files in the project.

src/NetteServicesNeonFinder.php:

final class NetteServicesNeonFinder
{
    /**
     * @param string $path
     * @return string[]
     */
    public function find(string $path): array
    {
        $serviceConfigs = Finder::findFiles('services.neon')->from($path);
        $neonFiles = [];
        foreach ($serviceConfigs as $serviceConfig) {
            /** @var SplFileInfo $serviceConfig */
            $neonFiles[] = $serviceConfig->getRealPath();
        }
        return $neonFiles;
    }
}

And then we just use it in the Dependency Injection Container loader.

src/bootstrap.php:

$loader = new ContainerLoader(__DIR__ . '/../temp', $isInDevelopmentMode);
$class = $loader->load(
    static function (Compiler $compiler) {
        $compiler->loadConfig(__DIR__ . '/config/config.local.neon');
        // ... rest of the configuration

        foreach ((new NetteServicesNeonFinder())->find(__DIR__) as $servicesNeon) {
            $compiler->loadConfig($servicesNeon);
        }
    },
);

$container =  new $class();

Because the container is cached in temp dir, we don't need to care about the cost of traversing the whole project for services.neon files. The only important thing is to do a warm-up of the cache before deploying into production.

Conclusion

This practice supports High Cohesion principle which says that code that belongs together should be close and thus by adopting this practice, we can boost the maintainability and scalability of the development of the application.

If you have any comment feel free to mention me on Twitter .

Found typo? Fix me!