Symfony autowiring monolog channels
Asked Answered
F

9

13

Following this documentation, I can create many channels which will create services with the following name monolog.logger.<channel_name>

How can I inject these services into my service with DI injection and autowiring ?

class FooService
{
    public function __construct(LoggerInterface $loggerInterface) {  }
}

Yaml

#existing
foo_service:
    class: AppBundle\Services\FooService
    arguments: ["@monolog.logger.barchannel"]
# what I want to do
foo_service:
    autowire: true # how to inject @monolog.logger.barchannel ? 
Foretoken answered 4/5, 2017 at 16:17 Comment(2)
The previous cookbook entry explains just that: Using a logger inside a service, see monolog.logger for an example.Uintathere
As far as I know you can't do that at the moment :( (Symfony 3.3). That would be nice having a DI on a Setter which parameter could be an existing defined service such as: "@monolog.logger.custom_channel" via annotation for instance. What I do at the moment is create a custom class for the logger, inject the "@monolog.logger.custom_channel" and then use autowiring in the class where I want to use the logger, so if DI Setter functionallity comes in the future adaptions will be done but autowiring will keep in the main class.Kirima
L
11

Starting from MonologBundle 3.5 you can autowire different Monolog channels by type-hinting your service arguments with the following syntax: Psr\Log\LoggerInterface $<channel>Logger. For example, to inject the service related to the app logger channel use this:

public function __construct(LoggerInterface $appLogger)
{
   $this->logger = $appLogger;
}

https://symfony.com/doc/current/logging/channels_handlers.html#monolog-autowire-channels

Latakia answered 23/1, 2020 at 10:2 Comment(3)
trying to get this working. using Symfony 5 (monolog-bundle 3.5) but always getting app channel logger injected despite any argument name combinations.Brochure
found out that this kind of feature only works for Symfony 4.2+ and therefore the channel in argument should be defined in monolog.channels configuration array. So that it will compile container using registration of alias for an argument feature.Brochure
for magic promised in documentation there is no code in bundle that will handle this despite tagging (as channel processing will be skipped if in tag there is no channel specified)Brochure
S
10

I wrote (maybe more complicated) method. I don't want to tag my autowired services to tell symfony which channel to use. Using symfony 4 with php 7.1.

I built LoggerFactory with all additional channels defined in monolog.channels.

My factory is in bundle, so in Bundle.php add

$container->addCompilerPass(
    new LoggerFactoryPass(), 
    PassConfig::TYPE_BEFORE_OPTIMIZATION, 
    1
); // -1 call before monolog

This is important to call this compiler pass before monolog.bundle because monolog after pass removes parameters from container.

Now, LoggerFactoryPass

namespace Bundle\DependencyInjection\Compiler;


use Bundle\Service\LoggerFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class LoggerFactoryPass implements CompilerPassInterface
{

    /**
     * You can modify the container here before it is dumped to PHP code.
     * @param ContainerBuilder $container
     * @throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
     * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
     */
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has(LoggerFactory::class) || !$container->hasDefinition('monolog.logger')) {
            return;
        }

        $definition = $container->findDefinition(LoggerFactory::class);
        foreach ($container->getParameter('monolog.additional_channels') as $channel) {
            $loggerId = sprintf('monolog.logger.%s', $channel);
            $definition->addMethodCall('addChannel', [
                $channel,
                new Reference($loggerId)
            ]);
        }
    }
}

and LoggerFactory

namespace Bundle\Service;

use Psr\Log\LoggerInterface;

class LoggerFactory
{
    protected $channels = [];

    public function addChannel($name, $loggerObject): void
    {
        $this->channels[$name] = $loggerObject;
    }

    /**
     * @param string $channel
     * @return LoggerInterface
     * @throws \InvalidArgumentException
     */
    public function getLogger(string $channel): LoggerInterface
    {
        if (!array_key_exists($channel, $this->channels)) {
            throw new \InvalidArgumentException('You are trying to reach not defined logger channel');
        }

        return $this->channels[$channel];
    }
}

So, now you can inject LoggerFactory, and choose your channel

public function acmeAction(LoggerFactory $factory)
{
    $logger = $factory->getLogger('my_channel');
    $logger->log('this is awesome!');
}
Shaina answered 29/5, 2018 at 9:41 Comment(1)
This is quite nice application of compiler passes, good job :) On the other hand, I prefer, that my services/actions/controllers do not know at all about available channels. It binds them closely with specific implementation. I strongly prefer to inject only LoggerInterface class and plan channels/injections/etc using configuration file. Your way will make testing harder, because you won't be able to just inject dummy logger into service constructor. You'll have to inject logger factory, and create this factory with correct channels and store channel names in code.Whitehall
D
8

After some searching I have found some kind of workaround using tags and manually injecting several parameters to autowired service.

My answer looks similar to @Thomas-Landauer. The difference is, I do not have to manually create logger service, as the compiler pass from monolog bundle does this for me.

services:
    _defaults:
        autowire: true
        autoconfigure: true
    AppBundle\Services\FooService:
        arguments:
            $loggerInterface: '@logger'
        tags:
            - { name: monolog.logger, channel: barchannel }
Decency answered 18/12, 2017 at 11:7 Comment(0)
E
8

You can use the bind parameter:

services:
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
        public: true
        bind:
            $loggerMyApi: '@monolog.logger.my_api'

Then you can use it in your service's constructor:

use Psr\Log\LoggerInterface;
...
public function __construct(LoggerInterface $loggerMyApi)
{
...
}
Eau answered 17/8, 2018 at 15:14 Comment(2)
However, if you bind the LoggerInterface service in _defaults, Symfony expects to find the parameter in every single service constructor! At least for me with Symfony 3.4. For example if I have a service that do not define the $loggerMyApi parameter, Symfony throw an error: Unused binding "$loggerMyApi" in service FooModerate
Then you should simply remove this bind it if it's not used.Winburn
H
5

I didn't find a way to autowire the very logger channel. However, I found a way to use autowire in principle, and inject just the logger manually. With your class FooService, this is how services.yml could look like (Symfony 3.3):

# services.yml

services:
    _defaults:
        autowire: true
        autoconfigure: true
    AppBundle\Services\FooService:
        arguments:
            $loggerInterface: '@monolog.logger.barchannel'

So the "trick" is to inject the logger channel explicitly, while still having all other dependencies of this service injected through autowiring.

Hermann answered 9/10, 2017 at 10:57 Comment(0)
D
1

From the documentation it is now possible to autowire based on the type hinting of the argument name.

// autowires monolog with "foo" channel
public function __construct(\Psr\Log\LoggerInterface $fooLogger);
Dulci answered 20/5, 2020 at 13:42 Comment(0)
C
0

Essentially, you've got two options:

First, service tagging:

services:
App\Log\FooLogger:
    arguments: ['@logger']
    tags:
        - { name: monolog.logger, channel: foo }

Then you can use your CustomLogger as a dependency elsewhere

Second, you can rely on Monolog to auto-register loggers for each custom channel within the configuration:

# config/packages/prod/monolog.yaml
monolog:
    channels: ['foo', 'bar']

You will then have these services available: monolog.logger.foo, 'monolog.logger.bar'

You can then retrieve them from the service container, or wire them in manually, e.g:

services:
App\Lib\MyService:
    $fooLogger: ['@monolog.logger.foo']

You can read more here and here.

Corot answered 5/10, 2018 at 12:33 Comment(2)
Not my downvote, but, while I guess that's a nice succinct explanation of channels, it does not answer how to make autowiring work with them.Obscurant
My upvote. this answer is correct, autowiring is not an issue here.Titograd
T
0

Recently I was implement single point access to the all registered loggers by MonologBundle. And also I tried to do some better solution - and did auto-generated logger decorators. Each class decorates one object of one of the registered monolog channel.

Link to the bundle adrenalinkin/monolog-autowire-bundle

Teage answered 28/4, 2019 at 21:36 Comment(0)
Z
0

For those still struggling with this one. In Symfony 4.3, I had, on top of that, add an alias for the specific channel, because without that, it was working only on the dev environment : when building, the Unit Tests were all failing because the custom logger was an undefined service.

 monolog.logger.my_custom_logger:         
   alias: Psr\Log\LoggerInterface         
   public: true 

 App\Logger\MyLogger:         
   arguments:             
     $logger: '@monolog.logger.my_custom_logger'
Zebapda answered 11/9, 2019 at 23:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.