咱們在寫代碼時,都想本身的代碼儘量的不影響現有的代碼。php
或者說,最大化不改動任何代碼的狀況下,如何嵌入咱們的新功能?這是咱們常說的「非侵入式」的開發方式。laravel
使用「非侵入式」的開發模式,主要在提供第三方插件和功能中最爲常見。今天藉助「Rollbar」第三方工具來講說如何作到「非侵入式」開發。json
本文主要能學到:bash
- Laravel Event / Listener 原理;
- Rollbar for Laravel 的使用
- 建立一個 Log to Dingding 羣的功能
在 Laravel,主要利用 EventServiceProvider
來加載 Events / Listeners
:session
<?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
類:app
<?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
。composer
咱們但願 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
傳入,用戶事件的註冊:ide
/** * 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 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
」,就能夠擴展咱們的功能。
參考