Laravel 的 Log 套件在 5.5 版之前,是使用 Writer 包裝 Monolog,成為一個 proxy pattern,被代理的類別則是寫死 Monolog。在 5.6 版開始,設計改採用 Logger 包裝 PSR-3 的 LoggerInterface,一樣是 proxy pattern,但被代理的類別只要是符合 PSR-3 的介面,就能使用。
後面一樣只會討論 5.7.6 版的原始碼。
Service provider 定義非常簡單,就直接初始化 LogManager:
$this->app->singleton('log', function () { return new LogManager($this->app); });
|
LogManager 實作了 LoggerInterface。它跟之前提到的 Day 10 有異區同工之妙,方法定義也非常接近,只差 LogManager 並沒有繼承 Manager 而已。
只要 logging.php 有設定好,基本上就可以直接 make 出來用:
app()->make('log')->info('something');
|
來看看 info() 裡面的實作:
public function info($message, array $context = []) { return $this->driver()->info($message, $context); }
|
非常簡單,取得 LoggerInterface driver 後,馬上再把 log 資訊轉交給 driver。這也是為何會說它是 proxy pattern 的主因--因為 LogManager 本身並沒有做跟 log 有關的事。
接著,看看 driver() 做了哪些事才能取得實例:
public function driver($driver = null) { return $this->get($driver ?? $this->getDefaultDriver()); }
|
getDefaultDriver() 是從 logging.php 裡取得設定,檔案註解裡有提到有下列幾種 driver:
- single
- daily
- slack
- syslog
- errorlog
- monolog
- custom
- stack
我們接著來看 single 與 stack 兩種 driver 怎麼透過 get() 產生吧:
protected function get($name) { try { return $this->channels[$name] ?? with($this->resolve($name), function ($logger) use ($name) { return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events'])); }); } catch (Throwable $e) { return tap($this->createEmergencyLogger(), function ($logger) use ($e) { $logger->emergency('Unable to create configured logger. Using emergency logger.', [ 'exception' => $e, ]); }); } }
|
這裡的 channels 指的是屬於 LogManager 的 channel,與 Monolog 無關。
with() 函式的等價程式碼如下:
$callable = function ($logger) { }
return $callbale($this->resolve($name));
|
Laravel 會有這些簡單的函式,最主要的用途是為了 function chain。比方說 with()->blah(); 就有辦法實現。
不過事實上,PHP 7.0 開始也支援下面的寫法,也是可以參考的:
return (function($logger) { })($this->resolve($name));
|
產生實例後,會使用 tap() 方法(跟之前提到的 tap() 函式是不同的)設定實例。
protected function tap($name, Logger $logger) { foreach ($this->configurationFor($name)['tap'] ?? [] as $tap) { list($class, $arguments) = $this->parseTap($tap);
$this->app->make($class)->__invoke($logger, ...explode(',', $arguments)); }
return $logger; }
protected function parseTap($tap) { return Str::contains($tap, ':') ? explode(':', $tap, 2) : [$tap, '']; }
|
從以上原始碼可以得知,tap 設定與 middleware 設定有點雷同,如:
class SomeTap { public function __invoke($logger, $param1, $param2) { } }
[ 'tap' => SomeTap::class . ':arg1,arg2', ];
|
簡單來說,這是一種用類別 __invoke() 來取代 tap() 功能的另解法。
tap() 裡面基本上是對 $logger 做一些初始化設定,比方說 Monolog 設定 processor 等。
再回頭過來看 resolve() 如何做:
protected function resolve($name) { $config = $this->configurationFor($name);
if (is_null($config)) { throw new InvalidArgumentException("Log [{$name}] is not defined."); }
if (isset($this->customCreators[$config['driver']])) { return $this->callCustomCreator($config); }
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) { return $this->{$driverMethod}($config); }
throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported."); }
|
從上面的程式碼來看,single 與 stack 會對應到的建置方法為:
- createSingleDriver
- createStackDriver
protected function createSingleDriver(array $config) { return new Monolog($this->parseChannel($config), [ $this->prepareHandler( new StreamHandler( $config['path'], $this->level($config), $config['bubble'] ?? true, $config['permission'] ?? null, $config['locking'] ?? false ) ), ]); }
protected function createStackDriver(array $config) { $handlers = collect($config['channels'])->flatMap(function ($channel) { return $this->channel($channel)->getHandlers(); })->all();
return new Monolog($this->parseChannel($config), $handlers); }
|
Stack 本身代表的意義是把所有 Laravel Logger 所定義的 channels(也就是 driver)整合成一個懶人包,只要對 stack 推送 log,所有在設定裡的 channel 都會跟著推送 log。
如果想針對單一 driver 推 log 也很簡單,使用 driver() 取得對應的 driver 即可。
雖然 Monolog 已經有內建 Registry,是類似概念的做法,但比起 Laravel 的 LogManager 還是缺了少了幾個元素,一個是設定轉成建實例的方法;另一個則是單元測試的易用性。
談到測試,Laravel 還是大勝許多套件,剩下沒幾天時間,希望有機會能談到測試。