理解Laravel中的pipeline
pipeline在laravel的啟動過程中出現次數很多,要了解laravel的啟動過程和生命周期,理解pipeline就是其中的一個關鍵點。網上對pipeline的講解很少,所以我自己寫一寫吧。
首先還是來看看調用棧,也就是從一個請求開始到返回響應,laravel都干了些什么。
在路由中配置如下
Route::get('/', function() {
return debug_backtrace();
});
然后啟動laravel自帶的web server
php artisan serve
然后訪問localhost:8000/
可以看到打印的調用棧,很長……
左下角可以看出,從請求開始到結束,盡管什么都沒干,但是依然加載了39個類……那些追求卓越性能的同學們需要好好考慮一下……
我們的重點是pipeline,如果你按照上面的步驟看了,會發現有一個pipeline調用的次數非常多。
那它到底是做什么用的呢?
簡單一點講,它是為了實現Laravel中的middleware。仔細想想middleware的代碼
public function handle($request, Closure $next) {
//do something for $request
return $next($request);
}
一個請求經歷層層的中間件的處理,才得到最終的請求,這到底是實現的呢?答案就是pipeline
首先來宏觀感受下pipeline,到底怎么用的,再去細說它
可以看到,主要有3個方法,也是pipeline暴露給使用者的所有方法
- send
- through
- then
send方法
/**
* Set the object being sent through the pipeline.
*
* @param mixed $passable
* @return $this
*/
public function send($passable) {
$this->passable = $passable;
return $this;
}
從說明看就是被在pipeline中傳遞的對象,可能不太好理解。那么我們就假定pipeline使用在middleware中,那么這個send的對象就是$request
想想看上面的中間件代碼,每個handle方法接收的第一個參數是$request,其實就是這兒設置的講被send的對象,它會在之后被到處傳遞,A處理完了送給B處理再送給C處理……pipeline,形象吧
through
顧名思義:通過、經由,就是上面send設置的對象要經由哪些中間件來處理
上面截圖宏觀說明pipeline的用法的時候可以看到,在調用through的時候
(new Pipeline($this))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
看through里傳的參數,很好懂,如果當前請求我們設置了不需要中間件,那么就傳一個空數組,不然就傳遞我們預先定義好的中間件數組
pipeline里的代碼如下
/**
* Set the array of pipes.
*
* @param dynamic|array $pipes
* @return $this
*/
public function through($pipes){
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
這個看起來也很簡單,你要么傳一個數組,要么傳多個字符串,它會拼成一個數組,無所謂,反正指明中間件的名字就好
then
接下來就是核心,也是代碼晦澀的地方了。
then方法就是當一切準備就緒后,下達開始處理的命令。
我們前面也能看到,send和through都是設置,并沒有做其他什么,所以當 要傳遞的對象、需要經歷的中間件都設置好之后,then?當然就是開始處理咯!
還是先看看then的代碼
/** * Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination){
$firstSlice = $this->getInitialSlice($destination);
$pipes = array_reverse($this->pipes);
return call_user_func(
array_reduce($pipes, $this->getSlice(), $firstSlice),
$this->passable);
);
}
看起來短短幾行,但是要理解起來可不太容易(這也是我比較喜歡laravel的原因,代碼簡單而優雅,一般人欣賞不來,哈哈)
我先大概講一下
then方法需要接收一個匿名函數 $destination,至于什么樣的匿名函數,你先可以不用管,往后看。我一句一句解釋
public function then(Closure $destination){
//把傳進來的匿名函數包裝成一個閉包,不太理解沒關系,反正知道$firstSlice是一個函數就好了
$firstSlice = $this->getInitialSlice($destination);
//把through方法傳進來的參數,也就是中間件數組反轉,為什么要這么做?
//簡單講是利用棧的特性,繼續往下看就知道了
$pipes = array_reverse($this->pipes);
//array_reduce(...)返回一個函數A,然后執行這個函數,把send進來的對象當參數傳遞函數A
return call_user_func(
array_reduce($pipes, $this->getSlice(), $firstSlice),
$this->passable);
);
}
然后呢?
沒有然后了,這就完成了整個工作,是不是很優雅(坑爹)的代碼,哈哈
那我們的精力就放到這個array_reduce上面,它到底返回了一個什么神奇的函數,我只要把$request傳遞給它,就過完了所有中間件?
array_reduce($arr, 'funcA',5)大致等價于,第二個參數可以是函數名的字符串
function fake_array_reduce($arr, $func, $initial=null)
$temp = $initial;
foreach($arr as $v) {
$temp = $func($temp, $v);
}
return $temp;
}
理解意思即可
回到then的源碼,代碼中傳遞的第二個參數一個是一個函數,而它是通過執行$this->getSlice()獲得的一個函數,我們來看看這個getSlice方法
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function getSlice(){
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) {
return call_user_func($pipe, $passable, $stack);
} else {
list($name, $parameters) = $this->parsePipeString($pipe);
return call_user_func_array(
[$this->container->make($name), $this->method],
array_merge([$passable, $stack], $parameters)
);
}
};
};
}
剛看到這里我也有點頭暈,這閉包套閉包的,什么鬼!
我們在簡化一下,就是調用$this->getSlice()之后得到了一個函數,假設是函數B,函數B接收兩個參數,并返回一個函數C
我們來大致模擬一下這個過程
寫一段假代碼,僅僅是為了更好地說明這個過程:
function haha($stack, $middleware) {
return function($param) use ($stack, $middleware){
//do something
};
}
function fake_array_reduce($middlewares, $func, $inital=null) {
$temp = $initial;
//假設傳遞進來的函數$func = 'haha'
foreach($middlewares as $middleware) {
$temp = $func($temp, $middleware);
}
return $temp;
}
這個過程的基本流程如上,haha返回了一個空函數(實際上肯定不是),我只是為了說明每次都返回一個函數作為下一次迭代的參數,而并沒有執行任何其它功能,按照上面的代碼,最后執行完fake_array_reduce會得到一個函數,這個函數接收一個參數。
所以再回過頭來看
call_user_func(array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable);
實際上就是相當于調用了array_reduce最后一次迭代返回的函數,然后給這個函數傳遞了一個參數$this->passable
所以,這時候我們需要關注最后一次返回的函數是什么,看看源碼關鍵是這段
return function($stack, $pipe) {
return function($passable) use ($stack, $pipe) {
if($pipe instanceof Closure) {
return call_user_func($pipe, $passable, $stack);
} else {
//..
}
}
}
每次迭代傳入了當前的$pipe和上一次迭代返回的函數
由上圖所示
由于把pipes先反轉了一下,所以最后一次迭代array_reduce得到的函數f3所use的是($stack=f2, $pipe=$middleware[0])
那么call_user_func(f3,$this->passable)相當于是
return call_user_func($middleware[0]->handle, $this->passable, f2);
仔細想想,middleware里面的handle方法
public function handle($request, Closure $next) {
//...
return $next($request);
}
在這里就相當于就是return f2($request)
也就相當于return call_user_func($middleware[1]->handle, $request, f1)
而這個$request是經過middleware[0]處理過后傳遞過來的。
……
這個過程比較繞,需要大家自己再畫圖理解一下,比如源代碼里用到的onion(洋蔥),$stack(棧),這些單詞可以幫助大家更好地理解這個過程。
所以return call_user_func($pipes, $this->getSlice(), $this->passable)就會把$request請求按middleware定義的先后順序,流水線處理,中間件處理完了之后再傳遞給$this->dispatchToRouter()這個方法處理(這又是一個抽象),從而得到最后的響應結果。
理解pipeline需要理解 閉包 棧等概念,熟悉了pipeline會對理解php的新特性有比較大的幫助。