How to log your life easier in Symfony?

How to log your life easier in Symfony?

because everyone deserves a logger-life!

·

4 min read

Hi coderz, how you doing? I know you are fed-up with blogposts and have tons of ToDos

image.png

So let's go for a quick blogpost and as usual I'll try to bring something useful (I hope) with as few words as I can (I hope as well).

The goal:

  • Turning any class in your project "logging-able", yet with the least possible changes and boilerplate verbosity.

The classical way:

  • So knowing that monolog's logger (however you configure it) is a tagged service at the end, the "only" way to "cleanly" use it is to DI/inject it to the needed service

  • Injecting services in Symfony can be done with multiple approaches and strategies. But in best cases you'll have at least to touch two things:

    • The class needing the logger by defining a private property to receive the logger service instance, and an assignement in the constructor to actually receive the service.

    • The services.yaml if you choose to go with setter injection rather than constructor injection (as the latter can benefit from autowiring)

  • Now as logging is a "nice and wanted" feature, your urge to log stuff here and there can grow over and over, and you might feel silly polluting your code with one more line of properties and one other in the constructor. Sometimes you'd only write a constructor for the sake eyes of the DI injection of the logger. Having this exact snippet copy/pasted over a dozen of classes might really make you feel unhappy.

The shortcut:

  • If you use the autocomplete feature of PHPStorm, you'd notice a pair of interface/trait having the same prefix.

    • Psr\Log\LoggerAwareInterface

    • Psr\Log\LoggerAwareTrait

  • Can we use them altogether to solve the issue above? → yes.
  • In general the SomethingAwareInterface naming pattern, means the class is supposed to have a method named "setSomething()".
  • And following conventions as well, having that setter means your class should have a "something" property that setter will modify, and here comes the SomethingAwareTrait to define that property for you.
  • Now implementing/using that interface/trait makes your class having a property named "something" and a setter for it. Nice thing here is that all that code verbosity is totally hidden in the backyard.
  • Still one single obstacle: How will the trait actually get the service instance.
  • One solution we might think about is the "@required" annotation above the setLogger() method, but in our case the setter is defined in the used trait, and we can't modify it.

image.png

  • A once and for all solution is to slightly modify the application's kernel, so that it loops over all services before the container is built, and check if any service is a "somethingAware", then make him really aware of it by explicitly injecting the service. Here is a showcase to make any class of your project that implement/use the pair above, being able to use the logger straight away without any further overhead:
<?php

// src/Kernel.php

namespace App;

use DateTime;
use Psr\Log\LoggerAwareInterface;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    public function process(ContainerBuilder $container): void
    {
        $definitions = $container->getDefinitions();
        foreach ($definitions as $definition) {
            if (!$this->isAware($definition, LoggerAwareInterface::class)) {
                continue;
            }
            $definition->addMethodCall(
                 'setLogger',
                [$container->getDefinition('monolog.logger')]
             );
        }
    }

 private function isAware(Definition $definition, string $awarenessClass): bool
 {
     $serviceClass = $definition->getClass();
     if ($serviceClass === null) {
         return false;
     }
     $implementedClasses = @class_implements($serviceClass, false);
     if (empty($implementedClasses)) {
         return false;
     }
     if (\array_key_exists($awarenessClass, $implementedClasses)) {
         return true;
     }

     return false;
 }

}

Now Just use implement the interface and use the trait in your command, for example, and you are ready to go!

<?php

// src/Command/MyCommand.php

declare(strict_types=1);

namespace App\Command;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
    name: 'app:my-command',
    description: 'test!',
)]
class MyCommand extends Command implements LoggerAwareInterface
{
    use LoggerAwareTrait;

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->logger->info('I can log!');

        return Command::SUCCESS;
    }
}
  • You can clone/download the code snippets above from this gist on github as well

Enough talk for today, hope it helped and see you soon!