[TOC]
前言
這篇文章我們開始講laravel框架中的門面Facade,什么是門面呢?官方文檔:
??Facades)為應用程序的服務容器中可用的類提供了一個「靜態」接口。Laravel 自帶了很多 facades ,幾乎可以用來訪問到 Laravel 中所有的服務。Laravel facades 實際上是服務容器中那些底層類的「靜態代理」,相比于傳統的靜態方法, facades 在提供了簡潔且豐富的語法同時,還帶來了更好的可測試性和擴展性。
??什么意思呢?首先,我們要知道laravel框架的核心就是個Ioc容器即[服務容器](https://d.laravel-china.org/docs/5.4/container),功能類似于一個工廠模式,是個高級版的工廠。laravel的其他功能例如路由、緩存、日志、數據庫其實都是類似于插件或者零件一樣,叫做[服務](https://d.laravel-china.org/docs/5.4/providers)。Ioc容器主要的作用就是生產各種零件,就是提供各個服務。在laravel中,如果我們想要用某個服務,該怎么辦呢?最簡單的辦法就是調用服務容器的make函數,或者利用依賴注入,或者就是今天要講的門面Facade。門面相對于其他方法來說,最大的特點就是簡潔,例如我們經常使用的Router,如果利用服務容器的make:
~~~
App::make('router')->get('/', function () {
return view('welcome');
});
~~~
如果利用門面:
~~~
Route::get('/', function () {
return view('welcome');
});
~~~
可以看出代碼更加簡潔。其實,下面我們就會介紹門面最后調用的函數也是服務容器的make函數。
# Facade的原理
??我們以Route為例,來講解一下門面Facade的原理與實現。我們先來看Route的門面類:
~~~
class Route extends Facade
{
protected static function getFacadeAccessor()
{
return 'router';
}
}
~~~
很簡單吧?其實每個門面類也就是重定義一下getFacadeAccessor函數就行了,這個函數返回服務的唯一名稱:router。需要注意的是要確保這個名稱可以用服務容器的make函數創建成功(App::make(‘router’)),原因我們馬上就會講到。
??那么當我們寫出Route::get()這樣的語句時,到底發生了什么呢?奧秘就在基類Facade中。
~~~
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
~~~
當運行Route::get()時,發現門面Route沒有靜態get()函數,PHP就會調用這個魔術函數__callStatic。我們看到這個魔術函數做了兩件事:獲得對象實例,利用對象調用get()函數。首先先看看如何獲得對象實例的:
~~~
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}
~~~
我們看到基類getFacadeRoot()調用了getFacadeAccessor(),也就是我們的服務重載的函數,如果調用了基類的getFacadeAccessor,就會拋出異常。在我們的例子里getFacadeAccessor()返回了“router”,接下來getFacadeRoot()又調用了resolveFacadeInstance()。在這個函數里重點就是
~~~
return static::$resolvedInstance[$name] = static::$app[$name];
~~~
我們看到,在這里利用了$app也就是服務容器創建了“router”,創建成功后放入$resolvedInstance作為緩存,以便以后快速加載。
??好了,Facade的原理到這里就講完了,但是到這里我們有個疑惑,為什么代碼中寫Route就可以調用Illuminate\Support\Facades\Route呢?這個就是別名的用途了,很多門面都有自己的別名,這樣我們就不必在代碼里面寫use Illuminate\Support\Facades\Route,而是可以直接用Route了。
# 別名Aliases
??為什么我們可以在laravel中全局用Route,而不需要使用use Illuminate\Support\Facades\Route?其實奧秘在于一個PHP函數:[class_alias](http://www.php.net/manual/zh/function.class-alias.php),它可以為任何類創建別名。laravel在啟動的時候為各個門面類調用了class_alias函數,因此不必直接用類名,直接用別名即可。在config文件夾的app文件里面存放著門面與類名的映射:
~~~
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
...
]
~~~
下面我們來看看laravel是如何為門面類創建別名的。
# 啟動別名Aliases服務
??說到laravel的啟動,我們離不開index.php:
~~~
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
...
~~~
??第一句就是我們前面說的composer的自動加載,接下來第二句獲取laravel核心的Ioc容器,第三句“制造”出Http請求的內核,第四句是我們這里的關鍵,這句牽扯很大,laravel里面所有功能服務的注冊加載,乃至Http請求的構造與傳遞都是這一句的功勞。
~~~
$request = Illuminate\Http\Request::capture()
~~~
這句是laravel通過全局$_SERVER數組構造一個Http請求的語句,接下來會調用Http的內核函數handle:
~~~
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$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);
}
event(new Events\RequestHandled($request, $response));
return $response;
}
~~~
在handle函數方法中enableHttpMethodParameterOverride函數是允許在表單中使用delete、put等類型的請求。我們接著看sendRequestThroughRouter:
~~~
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] :
$this->middleware)
->then($this->dispatchToRouter());
}
~~~
前兩句是在laravel的Ioc容器設置request請求的對象實例,Facade中清楚request的緩存實例。bootstrap:
~~~
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
~~~
?$bootstrappers是Http內核里專門用于啟動的組件,bootstrap函數中調用Ioc容器的bootstrapWith函數來創建這些組件并利用組件進行啟動服務。app->bootstrapWith:
~~~
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}
~~~
可以看到bootstrapWith函數也就是利用Ioc容器創建各個啟動服務的實例后,回調啟動自己的函數bootstrap,在這里我們只看我們Facade的啟動組件
~~~
\Illuminate\Foundation\Bootstrap\RegisterFacades::class
~~~
RegisterFacades的bootstrap函數:
~~~
class RegisterFacades
{
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance($app->make('config')->get('app.aliases', []))
->register();
}
}
~~~
可以看出來,bootstrap做了一下幾件事:
清除了Facade中的緩存
設置Facade的Ioc容器
獲得我們前面講的config文件夾里面app文件aliases別名映射數組
使用aliases實例化初始化AliasLoader
調用AliasLoader->register()
~~~
public function register()
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
protected function prependToLoaderStack()
{
spl_autoload_register([$this, 'load'], true, true);
}
~~~
??我們可以看出,別名服務的啟動關鍵就是這個spl_autoload_register,這個函數我們應該很熟悉了,在自動加載中這個函數用于解析命名空間,在這里用于解析別名的真正類名。
# 別名Aliases服務
??我們首先來看看被注冊到spl_autoload_register的函數,load:
~~~
public function load($alias)
{
if (static::$facadeNamespace && strpos($alias,
static::$facadeNamespace) === 0) {
$this->loadFacade($alias);
return true;
}
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
~~~
??這個函數的下面很好理解,就是class_alias利用別名映射數組將別名映射到真正的門面類中去,但是上面這個是什么呢?實際上,這個是laravel5.4版本新出的功能叫做實時門面服務。
# 實時門面服務
??其實門面功能已經很簡單了,我們只需要定義一個類繼承Facade即可,但是laravel5.4打算更近一步——自動生成門面子類,這就是實時門面。
??實時門面怎么用?看下面的例子:
~~~
amespace App\Services;
class PaymentGateway
{
protected $tax;
public function __construct(TaxCalculator $tax)
{
$this->tax = $tax;
}
}
~~~
這是一個自定義的類,如果我們想要為這個類定義一個門面,在laravel5.4我們可以這么做:
~~~
use Facades\ {
App\Services\PaymentGateway
};
Route::get('/pay/{amount}', function ($amount) {
PaymentGateway::pay($amount);
});
~~~
??當然如果你愿意,你還可以在alias數組為門面添加一個別名映射”PaymentGateway” => “use Facades\App\Services\PaymentGateway”,這樣就不用寫這么長的名字了。
??那么這么做的原理是什么呢?我們接著看源碼
~~~
protected static $facadeNamespace = 'Facades\\';
if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
$this->loadFacade($alias);
return true;
}
~~~
??如果命名空間是以Facades\開頭的,那么就會調用實時門面的功能,調用loadFacade函數:
~~~
protected function loadFacade($alias)
{
tap($this->ensureFacadeExists($alias), function ($path) {
require $path;
});
}
~~~
??[tap](https://segmentfault.com/a/1190000008447747)是laravel的全局幫助函數,ensureFacadeExists函數負責自動生成門面類,loadFacade負責加載門面類:
~~~
protected function ensureFacadeExists($alias)
{
if (file_exists($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) {
return $path;
}
file_put_contents($path, $this->formatFacadeStub(
$alias, file_get_contents(__DIR__.'/stubs/facade.stub')
));
return $path;
}
~~~
可以看出來,laravel框架生成的門面類會放到stroge/framework/cache/文件夾下,名字以facade開頭,以命名空間的哈希結尾。如果存在這個文件就會返回,否則就要利用file_put_contents生成這個文件,formatFacadeStub:
~~~
protected function formatFacadeStub($alias, $stub)
{
$replacements = [
str_replace('/', '\\', dirname(str_replace('\\', '/', $alias))),
class_basename($alias),
substr($alias, strlen(static::$facadeNamespace)),
];
return str_replace(
['DummyNamespace', 'DummyClass', 'DummyTarget'], $replacements, $stub
);
}
~~~
簡單的說,對于Facades\App\Services\PaymentGateway,$replacements第一項是門面命名空間,將Facades\App\Services\PaymentGateway轉為Facades/App/Services/PaymentGateway,取前面Facades/App/Services/,再轉為命名空間Facades\App\Services\;第二項是門面類名,PaymentGateway;第三項是門面類的服務對象,App\Services\PaymentGateway,用這些來替換門面的模板文件:
~~~
<?php
namespace DummyNamespace;
use Illuminate\Support\Facades\Facade;
/**
* @see \DummyTarget
*/
class DummyClass extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'DummyTarget';
}
}
~~~
替換后的文件是:
~~~
<?php
namespace Facades\App\Services\;
use Illuminate\Support\Facades\Facade;
/**
* @see \DummyTarget
*/
class PaymentGateway extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'App\Services\PaymentGateway';
}
}
~~~
就是這么簡單!!!
- 配置
- composer安裝
- composer用法
- composer版本約束表達
- phpstorm
- sftp文件同步
- php類型約束
- laradock
- 配置文件緩存詳解
- git
- 自定義函數
- 核心概念
- IOC
- 服務提供者
- Facade
- 契約
- 生命周期
- 路由
- 請求
- 命名路由
- 路由分組
- 資源路由
- 控制器路由
- 響應宏
- 響應
- Command
- 創建命令
- 定時任務
- console路由
- 執行用戶自定義的定時任務
- artisan命令
- 中間件
- 創建中間件
- 使用中間件
- 前置和后置
- 詳細介紹
- 訪問次數限制
- 為 VerifyCsrfToken 添加過濾條件
- 單點登錄
- 事件
- 創建
- ORM
- 簡介
- DB類
- 配置
- CURD
- queryScope和setAttribute
- 查看sql執行過程
- 關聯關系
- 一對一
- 一對多
- 多對多
- 遠程關聯
- 多態一對多
- 多態多對多
- 關聯數據庫的調用
- withDefault
- 跨模型更新時間戳
- withCount,withSum ,withAvg, withMax,withMin
- SQL常見操作
- 模型事件
- 模型事件詳解
- 模型事件與 Observer
- deleted 事件未被觸發
- model validation
- ORM/代碼片段
- Repository模式
- 多重where語句
- 中間表類型轉換
- Collection集合
- 新增的一些方法
- 常見用法
- 求和例子
- 機場登機例子
- 計算github活躍度
- 轉化評論格式
- 計算營業額
- 創建lookup數組
- 重新組織出表和字段關系并且字段排序
- 重構循環
- 其他例子
- 其他問題一
- 去重
- 第二個數組按第一個數組的鍵值排序
- 搜索ES
- 安裝
- 表單
- Request
- sessiom
- Response
- Input
- 表單驗證
- 簡介
- Validator
- Request類
- 接口中的表單驗證
- Lumen 中自定義表單驗證返回消息
- redis
- 廣播事件
- 發布訂閱
- 隊列
- 守護進程
- redis隊列的坑
- beanstalkd
- rabbitmq
- redis隊列
- 日志模塊
- 錯誤
- 日志詳解
- 數據填充與遷移
- 生成數據
- 數據填充seed
- migrate
- 常見錯誤
- Blade模板
- 流程控制
- 子視圖
- URL
- 代碼片段
- Carbon時間類
- 一些用法
- 郵件
- 分頁
- 加密解密
- 緩存
- 文件上傳
- 優化
- 隨記
- 嵌套評論
- 判斷字符串是否是合法的 json 字符串
- 單元測試
- 計算出兩個日期的diff
- 自定義一個類文件讓composer加載
- 時間加減
- 對象數組互轉方法
- 用戶停留過久自動退出登錄
- optional 輔助方法
- 文件下載
- Api
- Dingo api
- auth.basic
- api_token
- Jwt-Auth
- passport
- Auth
- Authentication 和 Authorization
- Auth Facade
- 授權策略
- Gates
- composer包
- debug包
- idehelp包
- image處理
- 驗證碼
- jq插件
- 第三方登錄
- 第三方支付
- log顯示包
- 微信包
- xss過濾
- Excel包
- MongoDB
- php操作
- 聚合查詢
- 發送帶附件郵件
- 中文轉拼音包
- clockwork網頁調試
- emoji表情
- symfony組件
- swooletw/laravel-swoole
- 常見問題
- 跨域問題
- Laravel隊列優先級的一個坑
- cache:clear清除緩存問題
- .env無法讀取
- 源碼相關基礎知識
- __set和__get
- 依賴注入、控制反轉和依賴倒置原則
- 控制反轉容器(Ioc Container)
- 深入服務容器
- call_user_func
- compact
- 中間件簡易實現
- array_reduce
- 中間件實現代碼
- Pipeline管道操作
- composer自動加載
- redis延時隊列
- 了解laravel redis隊列
- cli
- 源碼解讀
- Facade分析
- Facade源碼分析
- IOC服務容器
- 中間件原理
- 依賴注入淺析
- 微信
- 微信公眾號
- 常用接收消息
- 6大接收接口
- 常用被動回復消息
- 接口調用憑證
- 自定義菜單
- 新增素材
- 客服消息
- 二維碼
- 微信語音
- LBS定位
- 網頁授權
- JSSDK
- easywechat
- 小程序
- 小程序配置app.json