How use Laravel Contextual Binding for Logger channels?

1.4k Views Asked by At

In Laravel documentation I see example:

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when([VideoController::class, UploadController::class])
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

https://laravel.com/docs/8.x/container#contextual-binding

I want make the same with diffenet Log channels insead Storage disks.

https://laravel.com/docs/8.x/logging#writing-to-specific-channels

I try:

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
$this->app->when(PhotoController::class)
          ->needs('log')
          ->give(function () {
              return \Log::channel('telegram');
          });

$this->app->when([VideoController::class, UploadController::class])
          ->needs('log')
          ->give(function () {
              return \Log::channel('slack');
          });

But I get error:

NOTICE: PHP message: PHP Fatal error:  Uncaught Error: Maximum function nesting level of '256' reached, aborting! in /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:792
 Stack trace:
 #0 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(646): Illuminate\Foundation\Application->resolve('Illuminate\\Log\\...')
 #1 /var/www/html/app/Providers/AppServiceProvider.php(106): Illuminate\Container\Container->get('Illuminate\\Log\\...')
 #2 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(805): App\Providers\AppServiceProvider->App\Providers\{closure}(Object(Illuminate\Foundation\Application), Array)
 #3 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(691): Illuminate\Container\Container->build(Object(Closure))
 #4 /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(796): Illuminate\Container\Container->resolve('log', Array, true)
 #5 /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php(646):  in /var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Application.php on line 792

Also I tried:

->needs(LoggerInterface::class)

And the same error.

I do not see examples of how to do this correctly in the documentation. There is nothing about it.

1

There are 1 best solutions below

2
On

Sidenote: you can bind to an interface, its a common usage, Laravel calls them 'Contracts', see https://laravel.com/docs/8.x/container#binding-interfaces-to-implementations . So that is not the issue here.

Onto the question: I can't quite figure out what I'm doing differently but it works as intended for me:

use Illuminate\Support\Facades\Log;
$this->app->when(SomeController::class)->needs('log')->give(function() {
    return Log::channel('mychannel');
});

However, the 'log' string I dont quite like here. I believe it refers to the name of the so called "facade" for the logger, which brings more cognitive overload to what exactly this refers to. Imho its better to use a full class name here, to be more explicit:

use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Log;
$this->app->when(SomeController::class)->needs(Logger::class)->give(function() {
    return Log::channel('mychannel');
});

You can then use the following snippet in any of your controllers:

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

In fact you have a couple of options of defining the when/needs/give:

  • Illuminate\Log\Logger::class
  • Psr\Log\LoggerInterface::class (Logger above implements this)

Both work, but in both cases you need to use the exact class in your (to be injected) constructor exactly as written. You cannot bind the LoggerInterface and typehint Logger in your controller constructor and expect it to work, it does not recognize inheritance in this way apparently.

Another nice thing to know, is that apparently this only works on constructors directly. Controller methods have dependency injection as well but this does not work in combination with the contextual binding. However, it works with the default "simple" bindings as defined here https://laravel.com/docs/7.x/container#binding :

public function showUser(Illuminate\Log\Logger $log) {
    // This typehint does not take contextual binding into account. You will receive
    // the default binding for this class.
    $log->info('...');
}

See https://github.com/laravel/framework/issues/6177 (tl;dr: the issue is closed and contextual bindings for controller methods will not be supported in the future).

I hope this information helps you out.