說明
Thinkphp 6.0 RC5 開始使用了管道模式來實(shí)現(xiàn)中間件,比起之前版本的實(shí)現(xiàn)更加簡潔、有序。這篇文章對其實(shí)現(xiàn)細(xì)節(jié)進(jìn)行分析。
首先我們從入口文件 public/index.php 開始,$http = (new App())->http;
獲得一個(gè) http 類的實(shí)例后調(diào)用它的 run 方法:$response = $http->run();,然后它的 run 方法又調(diào)用了 runWithRequest 方法:
protected?function?runWithRequest(Request?$request) { ????. ????. ????. ????return?$this->app->middleware->pipeline() ????????->send($request) ????????->then(function?($request)?{ ????????????return?$this->dispatchToRoute($request); ????????}); }
中間件的執(zhí)行都在最后的 return 語句中。
立即學(xué)習(xí)“PHP免費(fèi)學(xué)習(xí)筆記(深入)”;
pipeline、through、send 方法
$this->app->middleware->pipeline()?的?pipeline?方法: public?function?pipeline(string?$type?=?'global') { ????return?(new?Pipeline())?? ???????????//?Array_map將所有中間件轉(zhuǎn)換成閉包,閉包的特點(diǎn): ??????????//?1.?傳入?yún)?shù):$request,請求實(shí)例;?$next,一個(gè)閉包 ??????????//?2.?返回一個(gè)Response實(shí)例 ????????->through(array_map(function?($middleware)?{ ????????????return?function?($request,?$next)?use?($middleware)?{ ????????????????list($call,?$param)?=?$middleware; ????????????????if?(is_array($call)?&&?is_string($call[0]))?{ ????????????????????$call?=?[$this->app->make($call[0]),?$call[1]]; ????????????????} ?????????????????//?該語句執(zhí)行中間件類實(shí)例的handle方法,傳入的參數(shù)是外部傳進(jìn)來的$request和$next ?????????????????//?還有一個(gè)$param是中間件接收的參數(shù) ????????????????$response?=?call_user_func($call,?$request,?$next,?$param); ????????????????if?(!$response?instanceof?Response)?{ ????????????????????throw?new?LogicException('The?middleware?must?return?Response?instance'); ????????????????} ????????????????return?$response; ????????????}; ????????????//?將中間件排序 ????????},?$this->sortMiddleware($this->queue[$type]????[]))) ????????->whenException([$this,?'handleException']); }
through 方法代碼:
public?function?through($pipes) { ????$this->pipes?=?is_array($pipes)???$pipes?:?func_get_args(); ????return?$this; }
前面調(diào)用 through 是傳入的 array_map(…) 把中間件封裝為一個(gè)個(gè)閉包,through 則是把這些閉包保存在 Pipeline 類的 $pipes 屬性中。
PHP 的 array_map 方法簽名:
array_map?(?callable?$callback?,?array?$array1?[,?array?$...?]?)?:?array
$callback 迭代作用于每一個(gè) $array 的元素,返回新的值。所以,最后得到 $pipes 中每個(gè)閉包的形式特征是這樣的(偽代碼):
function?($request,?$next)?{ ????$response?=?handle($request,?$next,?$param); ????return?$response; }
該閉包接收兩個(gè)參數(shù),一個(gè)是請求實(shí)例,一個(gè)是回調(diào)用函數(shù),handle 方法處理后得到相應(yīng)并返回。
through 返回一個(gè) Pipeline 類的實(shí)例,接著調(diào)用 send 方法:
public?function?send($passable) { ????$this->passable?=?$passable; ????return?$this; }
該方法很簡單,只是將傳入的請求實(shí)例保存在 $passable 成員變量,最后同樣返回 Pipeline 類的實(shí)例,這樣就可以鏈?zhǔn)秸{(diào)用 Pipeline 類的其他方法。
then,carry 方法
send 方法之后,接著調(diào)用 then 方法:
return?$this->app->middleware->pipeline() ????????????->send($request) ????????????->then(function?($request)?{ ????????????????return?$this->dispatchToRoute($request); ????????????});
這里的 then 接收一個(gè)閉包作為參數(shù),這個(gè)閉包實(shí)際上包含了控制器操作的執(zhí)行代碼。
then 方法代碼:
public?function?then(Closure?$destination) { ????$pipeline?=?array_reduce( ????????//用于迭代的數(shù)組(中間件閉包),這里將其倒序 ????????array_reverse($this->pipes), ????????//?array_reduce需要的回調(diào)函數(shù) ????????$this->carry(), ????????//這里是迭代的初始值 ????????function?($passable)?use?($destination)?{ ????????????try?{ ????????????????return?$destination($passable); ????????????}?catch?(Throwable?|?Exception?$e)?{ ????????????????return?$this->handleException($passable,?$e); ????????????} ????????}); ????return?$pipeline($this->passable); }
carry 代碼:
protected?function?carry() { ????//?1.?$stack?上次迭代得到的值,如果是第一次迭代,其值是后面的「初始值 ????//?2.?$pipe?本次迭代的值 ????return?function?($stack,?$pipe)?{ ????????return?function?($passable)?use?($stack,?$pipe)?{ ????????????try?{ ????????????????return?$pipe($passable,?$stack); ????????????}?catch?(Throwable?|?Exception?$e)?{ ????????????????return?$this->handleException($passable,?$e); ????????????} ????????}; ????}; }
為了更方便分析原理,我們把 carry 方法內(nèi)聯(lián)到 then 中去,并去掉錯(cuò)誤捕獲的代碼,得到:
public?function?then(Closure?$destination) { ????$pipeline?=?array_reduce( ????????array_reverse($this->pipes), ????????function?($stack,?$pipe)?{ ????????????return?function?($passable)?use?($stack,?$pipe)?{ ????????????????return?$pipe($passable,?$stack); ????????????}; ????????}, ????????function?($passable)?use?($destination)?{ ????????????return?$destination($passable); ????????}); ????return?$pipeline($this->passable); }
這里關(guān)鍵是理解 array_reduce 以及 $pipeline($this->passable) 的執(zhí)行過程,這兩個(gè)過程可以類比于「包洋蔥」和「剝洋蔥」的過程。
array_reduce 第一次迭代,$stack 初始值為:
(A)
function?($passable)?use?($destination)?{ ????return?$destination($passable); });
回調(diào)函數(shù)的返回值為:
(B)
function?($passable)?use?($stack,?$pipe)?{ ????return?$pipe($passable,?$stack); };
將 A 代入 B 可以得到第一次迭代之后的 $stack 的值:
(C)
function?($passable)?use?($stack,?$pipe)?{ ????return?$pipe($passable,? ????????function?($passable)?use?($destination)?{ ????????????return?$destination($passable); ????????}) ????); };
第二次迭代,同理,將 C 代入 B 可得:
(D)
//?偽代碼 //?每一層的$pipe都代表一個(gè)中間件閉包 function?($passable)?use?($stack,?$pipe)?{ ????return?$pipe($passable,??//倒數(shù)第二層中間件 ????????function?($passable)?use?($stack,?$pipe)?{ ????????????return?$pipe($passable,??//倒數(shù)第一層中間件 ????????????????function?($passable)?use?($destination)?{ ????????????????????return?$destination($passable);??//包含控制器操作的閉包 ????????????????}) ????????????); ????????}; ????); };
以此類推,有多少個(gè)中間件,就代入多少次,最后一次得到 $stack 就返回給 $pipeline。由于前面對中間件閉包進(jìn)行了倒序,排在前面的閉包被包裹在更里層,所以倒序后的閉包越是后面的在外面,從正序來看,則變成越前面的中間件在最外層。
層層包裹好閉包后,我們得到了一個(gè)類似洋蔥結(jié)構(gòu)的「超級」閉包 D,該閉包的結(jié)構(gòu)如上面的代碼注釋所示。最后把 $request 對象傳給這個(gè)閉包,執(zhí)行它:$pipeline($this->passable);,由此開啟一個(gè)類似剝洋蔥的過程,接下來我們看看這洋蔥是怎么剝開的。
剝洋蔥過程分析
array_map(…) 把每一個(gè)中間件類加工成一個(gè)類似這種結(jié)構(gòu)的閉包:
function?($request,?$next)?{ ????$response?=?handle($request,?$next,?$param); ????return?$response; }
其中 handle 是中間件中的入口,其結(jié)構(gòu)特點(diǎn)是這樣的:
public?function?handle($request,?$next,?$param)?{ ????//?do?sth?------?M1-1?/?M2-1 ????$response?=?$next($request); ????//?do?sth?------?M1-2?/?M2-2 ????return?$response; }
我們上面的「洋蔥」一共只有兩層,也就是有兩層中間件的閉包,假設(shè) M1-1,M1-2 分別是第一個(gè)中間件 handle 方法的前置和后值操作點(diǎn)位,第二個(gè)中間件同理,是 M2-1,M2-2。現(xiàn)在,讓程序執(zhí)行 $pipeline($this->passable),展開來看,也就是執(zhí)行:
//?偽代碼 function?($passable)?use?($stack,?$pipe)?{ ????return?$pipe($passable,?? ????????function?($passable)?use?($stack,?$pipe)?{ ????????????return?$pipe($passable,?? ????????????????function?($passable)?use?($destination)?{ ????????????????????return?$destination($passable);?? ????????????????}) ????????????); ????????}; ????); }($this->passable)
此時(shí),程序要求從:
return?$pipe($passable,?? ????function?($passable)?use?($stack,?$pipe)?{ ????????return?$pipe($passable,?? ????????????function?($passable)?use?($destination)?{ ????????????????return?$destination($passable);?? ????????????}) ????????); ????}; );
返回值,也就是要執(zhí)行第一個(gè)中間件閉包,$passable 對應(yīng) handle 方法的 $request 參數(shù),而下一層閉包
function?($passable)?use?($stack,?$pipe)?{ ????return?$pipe($passable,?? ????????function?($passable)?use?($destination)?{ ????????????return?$destination($passable);?? ????????}) ????); }
則對應(yīng) handle 方法的 $next 參數(shù)。
要執(zhí)行第一個(gè)閉包,即要執(zhí)行第一個(gè)閉包的 handle 方法,其過程是:首先執(zhí)行 M1-1 點(diǎn)位的代碼,即前置操作,然后執(zhí)行 $response = $next($request);,這時(shí)程序進(jìn)入執(zhí)行下一個(gè)閉包,$next($request) 展開來,也就是:
function?($passable)?use?($stack,?$pipe)?{ ????return?$pipe($passable,?? ????????function?($passable)?use?($destination)?{ ????????????return?$destination($passable);?? ????????}) ????); }($request)
依次類推,執(zhí)行該閉包,即執(zhí)行第二個(gè)中間件的 handle 方法,此時(shí),先執(zhí)行 M2-1 點(diǎn)位,然后執(zhí)行 $response = $next($request),此時(shí)的 $next 閉包是:
function?($passable)?use?($destination)?{ ????return?$destination($passable);?? })
屬于洋蔥之芯 —— 最里面的一層,也就是包含控制器操作的閉包,展開來看:
function?($passable)?use?($destination)?{ ????return?$destination($passable);?? })($request)
最終,我們從 return $destination($passable) 中返回一個(gè) Response 類的實(shí)例,也就是,第二層的 $response = $next($request) 語句成功得到了結(jié)果,接著執(zhí)行下面的語句,也就是 M2-2 點(diǎn)位,最后第二層閉包返回結(jié)果,也就是第一層閉包的 $response = $next($request) 語句成功得到了結(jié)果,然后執(zhí)行這一層閉包該語句后面的語句,即 M1-2 點(diǎn)位,該點(diǎn)位之后,第一層閉包也成功返回結(jié)果,于是,then 方法最終得到了返回結(jié)果。
整個(gè)過程過來,程序經(jīng)過的點(diǎn)位順序是這樣的:M1-1→M2-1→控制器操作→M2-2→M1-2→返回結(jié)果。
總結(jié)
整個(gè)過程看起來雖然復(fù)雜,但不管中間件有多少層,只要理解了前后兩層中間件的這種遞推關(guān)系,洋蔥是怎么一層層剝開又一層層返回的,來多少層都不在話下。