Diving into Symfony’s DependencyInjection — Part 3: Advanced uses

Alex Makdessi
ManoMano Tech team
Published in
7 min readOct 8, 2018

In this article, we’re going to dive into Symfony’s dependency injection component.
We will use it from scratch, out of the Symfony framework, with the minimum of external dependencies.
You can code along with me while reading this article, each step will be detailed so you can follow this article in the best possible way.

This article is divided into three posts:

First steps with the container
In this part you will learn how to setup a container from scratch and add definitions to it. We will create a simple security feature so we can interact with the container.

Symbiosis with the Config component
The DependencyInjection works well with the Config component. You’ll learn how to separate configuration in dedicated files to ease configuration and injection with autoconfiguration and autowiring.

Advanced uses
In this final part we’ll work with monolog to apply some advanced uses of the DependencyInjection component like service decoration. We’ll also learn how to cache the compiled container to improve our application performances.

Photo by NASA on Unsplash

Logging with monolog

Autoconfiguration through interfaces

I like to log what happens in my application to ease my debugging process. That’s why I want to add a logger in my voters. We are going to use the Monolog library.

composer require monolog/monolog

Since we require monolog alone, there is no logger service yet. We have to register it manually. Let’s create a config/monolog.php file, and load it after the config.php

According to monolog documentation, the Logger needs a channel, which is a string, and a handler. We are going to use a StreamHandler to log messages in the var/app.log file.

config/monolog.php

Don’t forget to load it in public/index.php

$loader->load('config.php');
$loader->load('monolog.php');

We could inject it with autowiring in our voters (through a LoggerInterface hinted constructor), but we are going to make something way better. We'll use the Psr\Log\LoggerAwareInterface : each class that implements this interface will see the logger injected with a setLogger method. To do this, we can use the ContainerBuilder::registerForAutoconfiguration() method

So let’s modify the config/monolog.php file:

config/monolog.php

Simply implement the LoggerAwareInterface in the PostVoter class. You can use the LoggerAwareTrait to do the implementation for you.

// ...
class PostVoter implements VoterInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
// ...
}

And that’s it, each class that will implement this interface will see the logger injected through the setLogger method. Let's try it :

public function vote(string $attribute, $subject, User $user): bool
{
$this->logger->debug(self::class . ' executed.');
// ...
}

A var/app.log file is created :

[2018-06-27 08:29:31] voter.DEBUG: App\Authorization\Voter\PostVoter executed. [] []
[2018-06-27 08:29:31] voter.DEBUG: App\Authorization\Voter\PostVoter executed. [] []
[2018-06-27 08:29:31] voter.DEBUG: App\Authorization\Voter\PostVoter executed. [] []
[2018-06-27 08:29:31] voter.DEBUG: App\Authorization\Voter\PostVoter executed. [] []

There are 4 lines, because in the PostController, we call the AccessManager::decide() function 4 times.

Handling parameters

During the logger configuration, we use some scalar values that can be used as application parameter, such as :

  • the logger channel for the voters
  • the path to the log file

The DI component allows us to set parameters, and use/inject them in our app. Let’s improve our configuration: we will create a config/parameters.php file that is loaded before all configuration files. It will handle all our scalar parameters.

config/parameters.php

Dont forget to load this new file

public/index.php

We declare 3 parameters:

  • root_dir, which is a helper to access the root dir of our application
  • app.logger.voter_channel, which is the logger channel used by our voters
  • app.logger.file_path, the path where we are going to save our logs.
    We can see the %foo% notation. It is a placeholder for a parameter. Here, we’re telling the container "Use the root_dir parameter here".

Let’s dump the container before and after the compilation to understand what happens:

// Before compilation
#parameters: array:3 [
"root_dir" => "/Users/alex.m/www/di/config/.."
"app.logger.voter_channel" => "voter"
"app.logger.file_path" => "%root_dir%/var/app.log"
]
#resolved: false
// After compilation
#parameters: array:3 [
"root_dir" => "/Users/alex.m/www/di/config/.."
"app.logger.voter_channel" => "voter"
"app.logger.file_path" => "/Users/alex.m/www/di/config/../var/app.log"
]
#resolved: true

So there must be a CompilerPass that resolves our parameters. It is the ResolveParameterPlaceHoldersPass.

Now, let’s use those freshly created parameters in the monolog.php config file!

config/monolog.php

The result is the same, but we now have a well designed configuration:

  • application parameters are stored in parameters.php
  • application configuration is handled in config.php
  • third party libraries configuration is handled in their own file

Service decoration

We’ve worked hard to achieve this result 💪🏼. Let’s have fun and decorate our logger with a FancyLoggerDecorator . This decorator will add some fancy emojis depending on the log level.

⚠️ This decorator is for educational purpose only. Please, do not reproduce at work.

The FancyLoggerDecorator will take as argument the decorated logger and implement the LoggerInterface. For each method, we'll prepend an emoji to the message to log.

src/Logger/FancyLoggerDecorator.php

Now, we need to register the FancyLoggerDecorator as a decorator for the LoggerInterface , and inject the original (decorated) logger into the decorator. The original id is automatically changed to decorator id + '.inner' by the DecoratorServicePass

config/monolog.php

Let’s try our new fancy logger:

$this->logger->debug('debug');
$this->logger->info('info');
$this->logger->notice('notice');
$this->logger->warning('warning');
$this->logger->error('error');
$this->logger->critical('critical');
$this->logger->alert('alert');
$this->logger->emergency('emergency');

And here is our output:

[2018-06-27 09:22:17] voter.DEBUG: 🤖 debug [] []
[2018-06-27 09:22:17] voter.INFO: ℹ️ info [] []
[2018-06-27 09:22:17] voter.NOTICE: 📝 notice [] []
[2018-06-27 09:22:17] voter.WARNING: ⚠️ warning [] []
[2018-06-27 09:22:17] voter.ERROR: ❌ error [] []
[2018-06-27 09:22:17] voter.CRITICAL: 🛑 critical [] []
[2018-06-27 09:22:17] voter.ALERT: 🚨 alert [] []
[2018-06-27 09:22:17] voter.EMERGENCY: 🆘 emergency [] []

Woah such a fancy logger!

Performance improvements

Caching the compiled configuration

For now, the container is built on each request. Because we handle the configuration with php files, and because we have a light configuration, it will not really impact performance. If you are using YAML or XML configuration (or even php), it is still highly recommended to cache your container.

On the first request, the container will be built and compiled, then we’ll dump its content in a file with the Symfony\Component\DependencyInjection\Dumper\PhpDumper.

public/index.php

Now, we’ll check if the file var/cache/CachedContainer.php exists. If it is the case we simply require it, if not we rebuild the container:

public/index.php

Try it yourself, your first request will take at least 3 seconds, the next one will be lightning fast!

ℹ️ The PhpDumper will create a CachedContainer class that extends Symfony's Container class. It's interesting to have a look at this class, you'll find your resolved parameters in the getDefaultParameters function or a function for your public services (getPostControllerService() in our case) :

Generated cached container — getPostControllerService() method

ℹ️ You can see that it will create a PostVoter, and inject the fancy logger in it through the setLogger method. Then it will create the AccessManager and add the voter to it. Finally, it will create the PostController, and because it is public, it will add it in the services array, with its FQCN as id.

DX improvement

The cached container is pretty cool. But if you try to add or remove a definition during your development, it will not be registered unless you clear your cache.

So we are going to create an env variable that can take dev or prod as a value. If this variable is equal to dev, we'll rebuild the container.

// public/index.php
$env = 'dev';
$cachedContainerFile = __DIR__ .'/../var/cache/CachedContainer.php';if ('prod' === $env && file_exists($cachedContainerFile)) {
// require the cached container
} else {
// build the container
}

💡 The best practice would be to create an environment variable and read it with the getenv() function, or the The Dotenv Component.

Autowiring arguments by name

Now that we have an environment feature, let’s add a parameter in the container called env to know in which environment our application is running.

// public/index.php
// ...
$container = new ContainerBuilder();
$container->addCompilerPass(new VoterPass());
// setting `env` parameter before config files are loaded.
$container->setParameter('env', $env);

Now, imagine you want to log the environment in the log context from your fancy logger. You want to inject this parameter in the fancy logger. Since it is a string, we can not autowire it. But Symfony’s DI allows us to autowire parameters through their variable name.

Back to the config/config.php. Remember the privateDefinition prototype that will be used for each autoconfigured definition? We'll add a binding to it, so the variable name $env will be resolved as the env parameter:

config/config.php

Now, simply add a $env variable in your PostVoter class and it will be autowired:

src/Authorization/Voter/PostVoter.php

Here’s the log output in development and production environments:

# dev
[2018-06-29 16:35:17] voter.DEBUG: 🤖– App\Authorization\Voter\PostVoter voter executed {"env":"dev"} []
# prod
[2018-06-29 16:36:44] voter.DEBUG: 🤖 App\Authorization\Voter\PostVoter voter executed {"env":"prod"} []

ℹ️ Note that you must clear your cached container to make it work for prod env. Simply delete the var/cache/CachedContainer.php file to generate a fresh one.

Last words

Dependency Injection is a simple yet useful pattern used in any programming language that helps you decouple your application code. This is also perfect to write stronger unit tests, as dependencies become easier to mock.

Symfony’s component is going further by using a powerful container that helps us managing our dependencies in many different ways. It is also shipped with an extension system, which allows third party bundles to share definitions with your container.

📖 If you want to learn more about this component capabilities, I highly recommend you to read the documentation pages available in Symfony’s doc:
https://symfony.com/doc/current/components/dependency_injection.html#learn-more

--

--