Laravel驅動管理類Manager的分析和使用
第一部分 概念說明
第二部分 Illuminate\Support\Manager源碼
第三部分 Manager類的使用
第一部分:概念解釋
結合實際解釋一下,啥是驅動:當我點了份外賣,那么外賣小哥無論如何都要講外賣送到我的手中,我不會關心小哥走的是絲綢之路,還是強者之路,更不會關心他是騎着飛機、坦克還是大炮送來的。我只要我的外賣到我的手中。
歸納一下,我點外賣要就要得到外賣,這就是契約,這就是接口規定的功能。
小哥走什么路線,什么交通工具是他自己的實現,也就是各種驅動。
是不是和laravel的契約和服務提供者概念很相似呢?
只不過今天要講解的Manager更加強調管理各個驅動,提前將所有的驅動全部注冊好,在使用的時候直接解析或者切換。
說到這道友們應該理解了,Manager能做的事情Container和Provider能夠做的更好。
那么Manager和Container、Provider的區別在哪呢?這里我只說明我的理解,Container和Provider更加的偏向框架層級,雖然也可以非侵入式的擴展和修改,但是相對Manager稍加麻煩,我將Manager看做一個小型的Container,里面包含了我要實現某個功能的各種驅動(實現),Manager更加的偏向業務邏輯層。
當要頻繁切換一個功能實現的時候(他更像一個頻繁更換內容,但是說明書不換的組件,俗話說的換湯不換葯),我可能會選擇Manager(比如發送短信,可以使用阿里大於,京東萬象,飛鴿等等),因為他更加輕量。當要實現一個系統級的服務的時候,我會選擇Container和Provider,比如上一篇中的日志服務。
第二部分:源碼說明
# 直接上代碼 挺簡單的一個類,基本可以見名知意。未展示屬性
<?php
namespace Illuminate\Support;
use Closure;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
// 值得注意的是 Manager是一個抽象類,一定要實現了其中的抽象方法getDefaultDriver才能實例化
// 我們觀察構造方法中的參數,你會不會想到在Provider中掛載Manager是一個好方法呢?
public function __construct(Container $container)
{
$this->app = $container;
$this->container = $container;
$this->config = $container->make('config');
}
// 此類是一個抽象類 這個方法用來返回默認的驅動名
abstract public function getDefaultDriver();
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
// 此處的static顯然是實際調用該方法的類
'Unable to resolve NULL driver for [%s].', static::class
));
}
// 有點類似單例的寫法
// 如果要解析的驅動已經解析過 那么直接返回
// 如果沒有解析過 那么解析 並掛載到類中
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
// 創建指定驅動
// 要注意此類中傳遞的$driver就是指定驅動的名字
protected function createDriver($driver)
{
// First, we will determine if a custom driver creator exists for the given driver and
// if it does not we will check for a creator method for the driver. Custom creator
// callbacks allow developers to build their own "drivers" easily using Closures.
# 官方注釋已經非常清晰了
# 如果要解析的驅動,是由我們手動通過鍵值對注冊進來的 那么就調用對應的閉包
# 否則觸發魔術方法__call
# 顯然Manager本類中並不存在額外的方法,所以魔術方法調用的方法,也要我們在子類中實現
# 以上就是兩種從Manager中返回驅動的方式了
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
// 上面說的通過此方法調用我們注冊進來的閉包 從而返回驅動
protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->container);
}
// 這個就是注冊閉包進來
// 你當然可以在業務邏輯中、甚至是指定的中間件中擴展你的Manager類
// 但我更喜歡在ServiceProvider的boot方法中進行擴展
public function extend($driver, Closure $callback)
{
$this->customCreators[$driver] = $callback;
return $this;
}
public function getDrivers()
{
return $this->drivers;
}
// __call魔術方法 從Manager中解析驅動的第二種方式
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
第三部分:使用(依然通過日志這個不恰當例子進行展示)
1 創建契約
<?php
namespace App\Contracts;
interface ManageLog
{
public function logCertains($level, $foo);
}
2 創建日志組件,使用管理器管理
<?php
namespace App\Components\Log;
use Illuminate\Support\Manager;
class LogManager extends Manager
{
// 這是個類,並且你可以通過$this->app拿到容器實例,也就意味着你可以做很多事情
public function getDefaultDriver()
{
// 你也可以將返回的字符串寫在配置中 等等
return 'elasticsearch';
}
// 展示魔術方法解析驅動
// $logManager->driver('elasticsearch')時觸發
public function createElasticsearchDriver()
{
// 上一篇有簡單示例
return '你的es日志驅動';
}
// 查看manager中的驅動
public function getCustomCreators()
{
return $this->customCreators;
}
}
3 創建不同的日志驅動
<?php
namespace App\Drivers\Log;
use App\Contracts\ManageLog;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Processor\MemoryPeakUsageProcessor;
use Monolog\Processor\MemoryUsageProcessor;
class RotateDriver implements ManageLog
{
protected $logger;
public function __construct()
{
$logger = new Logger('manager');
$rotatingHandler = new RotatingFileHandler(storage_path('logs/test/manager.log'), 7);
$logger->pushHandler($rotatingHandler);
// 隨便加點什么吧
$procesccor1 = new MemoryPeakUsageProcessor();
$procesccor2 = new MemoryUsageProcessor();
$logger->pushProcessor($procesccor1);
$logger->pushProcessor($procesccor2);
$this->logger = $logger;
}
public function logCertains($level, $foo)
{
$this->logger->{$level}($foo);
}
}
<?php
namespace App\Drivers\Log;
use App\Contracts\ManageLog;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class StreamDriver implements ManageLog
{
protected $logger;
public function __construct()
{
$logger = new Logger('manager');
$streamHandler = new StreamHandler(storage_path('logs/test/manager.log'));
$logger->pushHandler($streamHandler);
$this->logger = $logger;
}
public function logCertains($level, $foo)
{
$this->logger->{$level}($foo);
}
public function getLogger()
{
return $this->logger;
}
}
<?php
namespace App\Drivers\Log;
use App\Contracts\ManageLog;
use Illuminate\Foundation\Application;
use Monolog\Logger;
use Monolog\Handler\RedisHandler;
use Predis\Client;
class RedisDriver implements ManageLog
{
protected $logger;
public function __construct(Application $app)
{
// $logger = new Logger('manager');
// $redisClient = new Client('tcp://localhost:6379');
// $redisHandler = new RedisHandler($redisClient, 'manager');
// $logger->pushHandler($redisHandler);
// $this->logger = $logger;
// 利用monolog的重用機制
$customLoggers = $app->make('logManager')->getCustomCreators();
$streamLogger = call_user_func(array_shift($customLoggers));
$redisLogger = $streamLogger->getLogger()->withName('redis');
$redisClient = new Client('tcp://localhost:6379');
$redisHandler = new RedisHandler($redisClient, 'manager');
$redisLogger->pushHandler($redisHandler);
$this->logger = $redisLogger;
}
public function logCertains($level, $foo)
{
$this->logger->{$level}($foo);
}
}
4 創建服務提供者
# 此處說明一下 為什么使用singleton進行綁定,因為我在boot方法中兩次解析manager為了將其擴展,保證每次解析都是同一個manager,
# 也就修改了綁定到容器的manager,一旦在register方法中使用bind綁定的話,每次從容器中解析出來的都會是一個全新的manager,
# 也就是說我們的boot方法白白浪費了,也就自然不能夠進行任何的操作了。其實laravel為了解決這個問題還有其他方法,
# 請各位仔細查看服務提供者部分的文檔,我這里選擇在boot方法中對manager進行擴展,其實你可以在任何你喜歡的地方擴展。
php artisan make:provider LogManagerServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Components\Log\LogManager;
use App\Drivers\Log\StreamDriver;
use App\Drivers\Log\RotateDriver;
use App\Drivers\Log\RedisDriver;
class LogManagerServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->singleton('logManager', function ($app) {
# return new LogManager($app);
// 利用容器幫助我們解決類的依賴
return $app->make(LogManager::class);
});
$this->app->singleton(LogManager::class, function ($app) {
return new LogManager($app);
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
// 擴展我們的logmanager
$this->app['logManager']->extend('stream', function () {
return new StreamDriver();
});
$this->app['logManager']->extend('rotate', function () {
return new RotateDriver();
});
// dd($this->app['logManager']->getCustomCreators());
$this->app[LogManager::class]->extend('redis', function () {
// return new RedisDriver($this->app);
return $this->app->make(RedisDriver::class);
});
}
}
5 注冊服務
config/app.php
...
App\Providers\RouteServiceProvider::class,
// 注冊自定義日志服務
App\Providers\LogServiceProvider::class,
// 注冊日志管理服務
App\Providers\LogManagerServiceProvider::class,
6 使用測試
Route::get('logmanager', function (LogManager $logManager) {
resolve('logManager')->driver('stream')->logCertains('emergency', 'something emergency');
resolve('logManager')->driver('rotate')->logCertains('debug', 'debug something');
$logManager->driver('redis')->logCertains('info', 'aaa');
});
以上代碼比較簡單,各位領會精神就好,大家可以結合前面說過的facade,仿照laravel原生Log服務實現一個功能一致的log manager。
今天沒有下集預告,發現錯誤歡迎指正,感謝!!!