我們在寫程式碼時,都想自己的程式碼儘可能的不影響現有的程式碼。
或者說,最大化不改動任何程式碼的情況下,如何嵌入我們的新功能?這是我們常說的「非侵入式」的開發方式。
使用「非侵入式」的開發模式,主要在提供第三方外掛和功能中最為常見。今天藉助「Rollbar」第三方工具來說說如何做到「非侵入式」開發。
本文主要能學到:
- Laravel Event / Listener 原理;
- Rollbar for Laravel 的使用
- 建立一個 Log to Dingding 群的功能
Laravel Event / Listener 原理
在 Laravel,主要利用 EventServiceProvider
來載入 Events / Listeners
:
<?php
namespace Illuminate\Events;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;
class EventServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('events', function ($app) {
return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
});
}
}
複製程式碼
EventServiceProvider
返回的是 Dispatcher
物件。我們看看 Dispatcher
類:
<?php
namespace Illuminate\Events;
use Exception;
use ReflectionClass;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Container\Container;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastFactory;
use Illuminate\Contracts\Container\Container as ContainerContract;
class Dispatcher implements DispatcherContract
{
/**
* The IoC container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The registered event listeners.
*
* @var array
*/
protected $listeners = [];
/**
* The wildcard listeners.
*
* @var array
*/
protected $wildcards = [];
/**
* The queue resolver instance.
*
* @var callable
*/
protected $queueResolver;
/**
* Create a new event dispatcher instance.
*
* @param \Illuminate\Contracts\Container\Container|null $container
* @return void
*/
public function __construct(ContainerContract $container = null)
{
$this->container = $container ?: new Container;
}
/**
* Register an event listener with the dispatcher.
*
* @param string|array $events
* @param mixed $listener
* @return void
*/
public function listen($events, $listener)
{
foreach ((array) $events as $event) {
if (Str::contains($event, '*')) {
$this->setupWildcardListen($event, $listener);
} else {
$this->listeners[$event][] = $this->makeListener($listener);
}
}
}
...
}
複製程式碼
主要作用是繫結 Events
和 Listeners
,當 Events
觸發時,直接執行 Listeners
。
我們希望 log 除了在本地檔案儲存輸出外,也想把 log 資訊實時發到其他平臺和渠道上,這時候我們就需要藉助 LogServiceProvider
的 events / listeners
繫結實現了。現在來看看 LogServiceProvider
:
<?php
namespace Illuminate\Log;
use Monolog\Logger as Monolog;
use Illuminate\Support\ServiceProvider;
class LogServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('log', function () {
return $this->createLogger();
});
}
/**
* Create the logger.
*
* @return \Illuminate\Log\Writer
*/
public function createLogger()
{
$log = new Writer(
new Monolog($this->channel()), $this->app['events']
);
if ($this->app->hasMonologConfigurator()) {
call_user_func($this->app->getMonologConfigurator(), $log->getMonolog());
} else {
$this->configureHandler($log);
}
return $log;
}
...
}
複製程式碼
這裡將 $this->app['events']
也就是 Dispatcher
傳入,使用者事件的註冊:
/**
* Register a new callback handler for when a log event is triggered.
*
* @param \Closure $callback
* @return void
*
* @throws \RuntimeException
*/
public function listen(Closure $callback)
{
if (! isset($this->dispatcher)) {
throw new RuntimeException('Events dispatcher has not been set.');
}
$this->dispatcher->listen(MessageLogged::class, $callback);
}
複製程式碼
有了 ServiceProvider
和 listen
就可以做到「非入侵」開發了。
Rollbar
Rollbar error monitoring integration for Laravel projects. This library adds a listener to Laravel's logging component. Laravel's session information will be sent in to Rollbar, as well as some other helpful information such as 'environment', 'server', and 'session'.
簡單使用
使用該工具,只要在其官網註冊賬號,併產生一個 access token
即可
安裝該工具,也只需要簡單的兩步:
composer require rollbar/rollbar-laravel
// .env
ROLLBAR_TOKEN=[your Rollbar project access token]
// 如果 < Laravel 5.5,則需要在 app.php 中新增
Rollbar\Laravel\RollbarServiceProvider::class,
複製程式碼
測試,只要有 Log 輸出,rollbar 後臺都可以收到資訊,方便檢視,而再也不需要去看 log 檔案了。
剖析實現原理
我們來看看 rollbar 是不是我們所設想的那樣實現的?
我們先看看 RollbarServiceProvider
<?php namespace Rollbar\Laravel;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use Rollbar\Rollbar;
use Rollbar\Laravel\RollbarLogHandler;
class RollbarServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Bootstrap the application events.
*/
public function boot()
{
// Don't boot rollbar if it is not configured.
if ($this->stop() === true) {
return;
}
$app = $this->app;
// Listen to log messages.
$app['log']->listen(function () use ($app) {
$args = func_get_args();
// Laravel 5.4 returns a MessageLogged instance only
if (count($args) == 1) {
$level = $args[0]->level;
$message = $args[0]->message;
$context = $args[0]->context;
} else {
$level = $args[0];
$message = $args[1];
$context = $args[2];
}
$app['Rollbar\Laravel\RollbarLogHandler']->log($level, $message, $context);
});
}
/**
* Register the service provider.
*/
public function register()
{
// Don't register rollbar if it is not configured.
if ($this->stop() === true) {
return;
}
$app = $this->app;
$this->app->singleton('Rollbar\RollbarLogger', function ($app) {
$defaults = [
'environment' => $app->environment(),
'root' => base_path(),
'handle_exception' => true,
'handle_error' => true,
'handle_fatal' => true,
];
$config = array_merge($defaults, $app['config']->get('services.rollbar', []));
$config['access_token'] = getenv('ROLLBAR_TOKEN') ?: $app['config']->get('services.rollbar.access_token');
if (empty($config['access_token'])) {
throw new InvalidArgumentException('Rollbar access token not configured');
}
$handleException = (bool) array_pull($config, 'handle_exception');
$handleError = (bool) array_pull($config, 'handle_error');
$handleFatal = (bool) array_pull($config, 'handle_fatal');
Rollbar::init($config, $handleException, $handleError, $handleFatal);
return Rollbar::logger();
});
$this->app->singleton('Rollbar\Laravel\RollbarLogHandler', function ($app) {
$level = getenv('ROLLBAR_LEVEL') ?: $app['config']->get('services.rollbar.level', 'debug');
return new RollbarLogHandler($app['Rollbar\RollbarLogger'], $app, $level);
});
}
/**
* Check if we should prevent the service from registering
*
* @return boolean
*/
public function stop()
{
$level = getenv('ROLLBAR_LEVEL') ?: $this->app->config->get('services.rollbar.level', null);
$token = getenv('ROLLBAR_TOKEN') ?: $this->app->config->get('services.rollbar.access_token', null);
$hasToken = empty($token) === false;
return $hasToken === false || $level === 'none';
}
}
複製程式碼
這個比較好理解,先利用 register
註冊兩個 singleton
,然後在 boot
方法中,註冊 listener
$app['log']->listen(function () use ($app){});
複製程式碼
其中 $app['log']
,就是我們的上文說的 LogServiceProvider
,將 listener
註冊到 EventServiceProvider
中。
$this->dispatcher->listen(MessageLogged::class, $callback);
複製程式碼
最後我們看看 Rollbar
facades 返回的是:RollbarLogHandler
物件
<?php namespace Rollbar\Laravel\Facades;
use Illuminate\Support\Facades\Facade;
class Rollbar extends Facade
{
/**
* Get a schema builder instance for the default connection.
*
* @return \Rollbar\Laravel\RollbarLogHandler
*/
protected static function getFacadeAccessor()
{
return 'Rollbar\Laravel\RollbarLogHandler';
}
}
複製程式碼
看看 RollbarLogHandler
實現,也主要是將 log 資訊反饋到Rollbar 中,此處不做分析了。
模擬實現
通過對 Rollbar
簡單的分析,就會發現原來通過簡單 Listener
,不用改現在的任何功能和程式碼,就能實現將 log 實時發到你想接收的地方。
所以我們可以嘗試也寫一個這樣的功能,將 log 資訊發到釘釘上。
好了,我們開始寫 Log2Dingding
外掛。
根據之前的文章我們可以很方便的組織好外掛結構:
composer.json
設定:
{
"name": "fanly/log2dingding",
"description": "Laravel Log to DingDing",
"license": "MIT",
"authors": [
{
"name": "fanly",
"email": "yemeishu@126.com"
}
],
"require": {},
"extra": {
"laravel": {
"providers": [
"Fanly\\Log2dingding\\FanlyLog2dingdingServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Fanly\\Log2dingding\\": "src/"
}
}
}
複製程式碼
我們定義 ServiceProvider
:
<?php
/**
* User: yemeishu
* Date: 2018/5/13
* Time: 下午2:56
*/
namespace Fanly\Log2dingding;
use Fanly\Log2dingding\Dingtalk\Messager;
use Illuminate\Support\ServiceProvider;
use Fanly\Log2dingding\Support\Client;
class FanlyLog2dingdingServiceProvider extends ServiceProvider {
protected function registerFacade()
{
// Don't register rollbar if it is not configured.
if ($this->stop() === true) {
return;
}
$this->app->singleton('fanlylog2dd', function ($app) {
$config['access_token'] = getenv('FANLYLOG_TOKEN') ?: $app['config']->get('services.fanly.log2dd.access_token');
if (empty($config['access_token'])) {
throw new InvalidArgumentException('log2dd access token not configured');
}
return (new Messager(new Client()))->accessToken($config['access_token']);
});
}
/**
* Bootstrap the application services.
*/
public function boot()
{
// Don't boot rollbar if it is not configured.
if ($this->stop() === true) {
return;
}
$app = $this->app;
// Listen to log messages.
$app['log']->listen(function () use ($app) {
$args = func_get_args();
// Laravel 5.4 returns a MessageLogged instance only
if (count($args) == 1) {
$level = $args[0]->level;
$message = $args[0]->message;
$context = $args[0]->context;
} else {
$level = $args[0];
$message = $args[1];
$context = $args[2];
}
$app['fanlylog2dd']->message("[ $level ] $message\n".implode($context))->send();
});
}
/**
* Register the application services.
*/
public function register()
{
$this->registerFacade();
}
private function stop()
{
$level = getenv('FANLYLOG_LEVEL') ?: $this->app->config->get('services.rollbar.level', null);
$token = getenv('FANLYLOG_TOKEN') ?: $this->app->config->get('services.rollbar.access_token', null);
$hasToken = empty($token) === false;
return $hasToken === false || $level === 'none';
}
}
複製程式碼
我們主要是建立一個發釘釘訊息的單例,然後再註冊 listener
,只要獲取 log 資訊,就傳送資訊到釘釘上。
測試一下:
總結
最後做成外掛,和 Rollbar
一樣,引入:
composer require "fanly/log2dingding"
// .env
FANLYLOG_TOKEN=56331868f7056a3e645e7dba034c5550e7af***
複製程式碼
同樣的,其他資訊都不需要設定,跑一個測試:
Laravel 框架的一大好處在於,可以以友好的方式實現我們「非入侵」開發,只要藉助「ServiceProvider
」和「Events/Listner
」,就可以擴充套件我們的功能。
參考
- 「12步」製作 Laravel 外掛 (一)mp.weixin.qq.com/s/AD05BiKjP…
- 「3步」釋出 Laravel 外掛 (二)mp.weixin.qq.com/s/RSYeHU7aR…
- fanly/log2dingding packagist.org/packages/fa…