Laravel Facade原理及使用
laravel過於龐大,加之筆者水平有限,所以后面的源碼解讀會按模塊功能介紹,希望能幫大家稍微捋順下思路,即使能夠幫助大家回顧幾個函數也好。如發現錯誤,還望指正。
- facade工作方式,允許我們可以通過靜態調用的方式直接使用容器中的服務
- 原理講解,在laravel的routes/web.php等路由文件下,經常可以看到類似的寫法
<?php
Route::get('zbc', function () {
app()->make(App\Http\Controllers\Zbc\TestController::class);
});
// 可以看到通過Route::get方法添加了此路由規則(不僅如此laravel存在大量這般的靜態調用方式),但是並沒有看到此文件中引用Route類,並且laravel框架中並沒有此Route類,
// 通過打印get_declared_classes確實存在此Route(route)類,只有一個解釋,那就是laravel引導過程中‘生成了’這個類,下面就會講解如何‘生成’的這個類,這個類是根據什么‘生成’的。
- 上文講到在index.php中拿到了laravel的’黑盒子‘$kernel,下面繼續看kernel的handle方法
// Illuminate\Foundation\Http\Kernel文件中
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
// 這里的方法大家看名字就能大概知道什么用處,本問只講解sendRequestThroughRouter中的facade注冊部分
try {
$request->enableHttpMethodParameterOverride();
// 通過路由或者中間件處理給定的請求
// 跳轉到sendRequestThroughRouter方法
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
// 引導app
// 跳轉到bootstrap方法
$this->bootstrap();
// laravel的pipeline以后會專門講解
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
// $this->app是make的時候通過構造方法注入進來的(參考上篇文章)
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
// 如果laravel不能存在引導完成標志,就進行引導
// 跳轉到bootstrapWith方法,傳遞的參數如下
// protected $bootstrappers = [
// \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
// \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
// \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
// 我們的facade'生成'(注冊)就在此完成
// \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
// \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
// \Illuminate\Foundation\Bootstrap\BootProviders::class,
// ];
$this->app->bootstrapWith($this->bootstrappers());
}
}
// Application下的bootstrapWith方法
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->dispatch('bootstrapping: ' . $bootstrapper, [$this]);
// 本文只講解facaderegister
// 所以此處應該的$$bootstrapper = Illuminate\Foundation\Bootstrap\RegisterFacades
// 如果看過前面的文章可以知道容器並沒有綁定過此abstract更不可能存在解析過的instance
// 所以容器的make方法走的一定是php的反射機制,然后調用bootstrap方法
// 跳轉到RegisterFacades的bootstrap方法
$this->make($bootstrapper)->bootstrap($this);
$this['events']->dispatch('bootstrapped: ' . $bootstrapper, [$this]);
}
}
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
// 跳轉到getInstance方法
AliasLoader::getInstance(array_merge(
// 拿到config/app.php下的aliases數組 如下
// 'App' => Illuminate\Support\Facades\App::class,
// 'Arr' => Illuminate\Support\Arr::class,
// ...
// 'Route' => Illuminate\Support\Facades\Route::class,
// ...
// 'Validator' => Illuminate\Support\Facades\Validator::class,
// 'View' => Illuminate\Support\Facades\View::class,
$app->make('config')->get('app.aliases', []),
// 需要修改composer.json文件 配合包自動發現 這個類就是這個用處的
$app->make(PackageManifest::class)->aliases()
))->register();
}
// Illuminate\Foundation\AliasLoader類
// 方法很簡單
public static function getInstance(array $aliases = [])
{
if (is_null(static::$instance)) {
return static::$instance = new static($aliases);
}
$aliases = array_merge(static::$instance->getAliases(), $aliases);
static::$instance->setAliases($aliases);
return static::$instance;
}
// 繼續看register方法
/**
* Register the loader on the auto-loader stack.
*
* @return void
*/
public function register()
{
if (!$this->registered) {
// 繼續跳轉prependToLoaderStack到方法
$this->prependToLoaderStack();
$this->registered = true;
}
}
// 可以看到此方法在加載函數隊列首部添加了一個load加載函數
// spl_autoload_register方法的參數在composer第一篇有講解
protected function prependToLoaderStack()
{
// 跳轉到load方法
spl_autoload_register([$this, 'load'], true, true);
}
/**
* Load a class alias if it is registered.
*
* @param string $alias
* @return bool|null
*/
public function load($alias)
{
if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
$this->loadFacade($alias);
return true;
}
// 由於
if (isset($this->aliases[$alias])) {
// 重點!!!
// 在此案例中$alias傳遞進來的是我們在路由文件web.php中使用的Route
return class_alias($this->aliases[$alias], $alias);
}
// 為了方便理解,可做如下簡單修改
// config/app.php的aliases數組中存在Route映射 條件表達式為true
if (isset($this->aliases[$alias])) {
dump($alias); // 打印為Route,就是我們在web.php中使用的Route
dump(get_declared_classes()); // 一堆類名組成的數組 但是不包括route
// 關鍵在此函數 class_alias 第一個參數是original class 第二個參數是給這個類起的別名
// 最重要的第三個參數默認為true 表示如果原類沒找到 是否可以通過別名自動加載這個類
// class_alias返回bool
// 返回的只是bool值,load加載器並沒有真正的引入實際的Illuminate\Support\Facades\Route類
// 所以php會繼續調用composer的加載器真正的加載此類,此加載器只是取巧的設置了別名,方便使用
class_alias($this->aliases[$alias], $alias);
dump(get_declared_classes()); // 一堆類名組成的數組 但是包括了route
return true;
}
}
// 下面看Illuminate\Support\Facades\Route類
// 非常簡單只有一個方法,並沒有發現web.php中的Route::get方法,便會觸發php的魔術方法__callStatic(請在自省查閱手冊),這是laravel facade靜態調用實現的根本方式
// 跳轉到父類Facade
class Route extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'router';
}
}
// Illuminate\Support\Facades\Facade類
public static function __callStatic($method, $args)
{
// 使用static關鍵字,實現延遲綁定,此案例中代表Illuminate\Support\Facades\Route
// 跳轉到getFacadeRoot方法
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
/**
* Get the root object behind the facade.
*
* @return mixed
*/
// 官方注釋已經很完美了,獲取門面背后的真實對象
public static function getFacadeRoot()
{
// 注意使用的是static 靜態延遲綁定 此案例中代表Illuminate\Support\Facades\Route
// 跳轉到resolveFacadeInstance方法
return static::resolveFacadeInstance(static::getFacadeAccessor());
// return static::resolveFacadeInstance('router');
}
/**
* Resolve the facade root instance from the container.
*
* @param object|string $name
* @return mixed
*/
// 從容器中解析門面對應的根對象
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
// $app為Application對象但是為什么采用數組的形式進行訪問呢($app['router'])
// 因為Application類繼承了Container類,而Container實現了spl類庫提供的ArrayAccess接口
// 關於ArrayAccess請自行查閱文檔
// 當通過數組的形式訪問對象的時候 會觸發offsetGet方法,跳轉到Container的offsetGet方法
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
/**
* Get the value at a given offset.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key)
{
// $key在此案例中等於router
// 通過容器進行解析router
// router在Application中實例化的registerBaseServiceProvider中實現的注冊,前面的文章有講解
// 一路返回到__callStatic方法中
return $this->make($key);
}
public static function __callStatic($method, $args)
{
// 使用static關鍵字,實現延遲綁定,此案例中代表Illuminate\Support\Facades\Route
// 跳轉到getFacadeRoot方法
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
// 到此調用$router的get方法 完美!!!
// 對應的方法看這里 Illuminate\Routing\Router::get
return $instance->$method(...$args);
}
以上便是laravel facade的基本實現方式,大家可能已經看出來這就是php實現門面(代理)模式的方法。
個人不喜歡使用門面模式,更喜歡直接調用系統函數等方式直接從容器中解析對象,感覺可以規避一下__callStatic的調用。
下面講解如何在laravel中使用自己的facade
1. 創建契約
<?php
namespace App\Contracts;
interface MyFacade
{
public function thereYouGo();
}
2. 創建服務
<?php
namespace App\Services;
use App\Contracts\MyFacade;
class TestService implements MyFacade
{
public function thereYouGo()
{
echo '春風習習可盈袖, 不及伊人半點紅';
}
}
3. 創建服務提供者 php artisan make:provider MyFacadeProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\TestService;
class MyFacadeProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
// 注意此處的$abstract(MyLove)要和facade中getFacadeAccessor方法返回值一致
$this->app->bind('MyLove', function () {
return new TestService();
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
4. 創建門面
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class MyFacade extends Facade
{
protected static function getFacadeAccessor()
{
return 'MyLove';
}
}
5. 注冊服務和門面 config/app.php下添加
'providers' => [
...
App\Providers\MyFacadeProvider::class,
],
'aliases' => [
...
'MyFacade' => App\Services\TestService::class,
]
6. 測試
use App\Facades\MyFacade;
Route::get('myfacade', function () {
MyFacade::thereYouGo();
});
可以看到實現一個facade真的費時費力,並且性能不好,不建議自行創建facade使用,更建議使用容器直接解析,當然硬編碼可能更適合追求速度的開發,不管怎樣開心擼碼最重要。
今天沒有下集預告