DEV Community

Cover image for How to log your life easier in Symfony?
medunes
medunes

Posted on

How to log your life easier in Symfony?

Hi coderz, how you doing? I know you are fed-up with blogposts and have tons of work, yet, check this!

image.png

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;
 }

}
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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!

Top comments (1)

Collapse
 
boolean_type_93f6ab3d906b profile image
Boolean Type • Edited

Nice approach, I used it for a long time. But today I've noticed this statement in Symfony docs:

If your application uses service autoconfiguration, any service whose class implements Psr\Log\LoggerAwareInterface will receive a call to its method setLogger() with the default logger service passed as a service.

It looks like we don't need to modify the application's kernel! I removed process() and isAware(), so I just:

  • implement LoggerAwareInterface / LoggerAwareTrait in my services;
  • add LoggerInterface::class as subscribed service in my controllers, like this:
#[\Override]
public static function getSubscribedServices(): array
{
        return \array_merge(parent::getSubscribedServices(), [
            'logger' => '?' . LoggerInterface::class,
        ]);
}
Enter fullscreen mode Exit fullscreen mode

And logging still works.

But I think, your solution is very convenient in case if we realize our custom logger service (not the default one).