理解Laravel中的pipeline

jopen 8年前發布 | 17K 次閱讀 Web框架

pipeline在laravel的啟動過程中出現次數很多,要了解laravel的啟動過程和生命周期,理解pipeline就是其中的一個關鍵點。網上對pipeline的講解很少,所以我自己寫一寫吧。
首先還是來看看調用棧,也就是從一個請求開始到返回響應,laravel都干了些什么。
在路由中配置如下

Route::get('/', function() {
    return debug_backtrace();
});

然后啟動laravel自帶的web server

php artisan serve

啟動laravel自帶的serve


然后訪問localhost:8000/
可以看到打印的調用棧,很長……


打印調用棧


左下角可以看出,從請求開始到結束,盡管什么都沒干,但是依然加載了39個類……那些追求卓越性能的同學們需要好好考慮一下……
我們的重點是pipeline,如果你按照上面的步驟看了,會發現有一個pipeline調用的次數非常多。
那它到底是做什么用的呢?
簡單一點講,它是為了實現Laravel中的middleware。仔細想想middleware的代碼

public function handle($request, Closure $next) {
    //do something for $request
    return $next($request);
}

一個請求經歷層層的中間件的處理,才得到最終的請求,這到底是實現的呢?答案就是pipeline
首先來宏觀感受下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


屏幕快照 2015-09-07 下午9.05.36.png


顧名思義:通過、經由,就是上面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方法就是當一切準備就緒后,下達開始處理的命令。
我們前面也能看到,sendthrough都是設置,并沒有做其他什么,所以當 要傳遞的對象、需要經歷的中間件都設置好之后,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和上一次迭代返回的函數


IMG_0687.JPG


由上圖所示
由于把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的新特性有比較大的幫助。

That's all thanks

來自: http://www.jianshu.com/p/3c2791a525d0

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!