Diving into Symfony’s DependencyInjection — Part 3: Advanced uses
By: Alex Makdessi
Tags: Backend, PHP, Web Development, Symfony, Dependency Injection

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

, 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

, 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!