徹底弄懂 Laravel 中間件原理


Laravel 的中間件機制提供了一種管道的方式,每個 HTTP 請求經過一個又一個中間件進行過濾,Laravel 內置了很多中間件,比如 CSRF 機制,身份認證,Cookie 加密,設置 Cookie 等等。

本文就來探究 Laravel 中間件的實現原理,看 Laravel 如何把 PHP 的 array_reduce 函數和閉包用到了極致。

需要先了解 Laravel 中間件的用法,如何定義一個中間件,還有前置中間件,后置中間件的概念。(文檔: Laravel 5.5 中間件

開始

為了徹底弄懂 Laravel 中間件原理,可以構造一個路由,並使用 debug_backtrace 函數來打印方法調用過程。

Route::get('test',function(){
   dump(debug_backtrace());
});

如圖,可見許多地方都跟 Pipeline 組件有關,並且重復執行一個閉包方法。

這里 pipes 數組就是需要用到的中間件。

中間件核心類 Pipeline

在 Laravel 框架 index.php 入口文件里,$kernel->handle() 方法就調用了 Pipeline 的方法,可以說它是貫穿始終的,這是把請求發到中間件進行處理的方法:

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
protected function sendRequestThroughRouter($request){

    $this->app->instance('request', $request);
 
    Facade::clearResolvedInstance('request');
 
    $this->bootstrap();
 
    return (newPipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware()?[]: $this->middleware)
                ->then($this->dispatchToRouter());
}

其中 send 方法是設置 passable 屬性也就是 $request,through 是設置 pipes 屬性,也就是需要用到的中間件,是一個數組,重點是這里的 then 方法,參數也是一個閉包函數。

/**
 * Run the pipeline with a final destination callback.
 *
 * @param  \Closure  $destination
 * @return mixed
 */
public function then(Closure $destination)
{
  $pipeline = array_reduce(
      array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
  );

  return $pipeline($this->passable);
}

array_reduce 使用

這里就需要講解一下 array_reduce 的用法了,可以說是妙用,這是理解 Laravel 中間件的重點,深刻領會了它的用法,就弄懂了 Laravel 中間件的原理。

先看一個官方例子明白它的基本用法:

function sum($carry, $item){
    $carry += $item;
    return $carry;}
 
$a = array(1,2,3,4,5);
 
var_dump(array_reduce($a,"sum"));// int(15)

基礎用法參考 PHP 文檔: array_reduce

這是一個最簡單的例子,array_reduce 會迭代每個元素,回調函數第一個參數是上次執行的結果,然后返回最終的一個值。

那么第二個參數的回調函數返回的是一個閉包呢?

$arr =['AAAA','BBBB','CCCC'];
 
$res = array_reduce($arr,function($carry, $item){
    return function() use ($carry,$item){
        dump("item:".$item);
        if(is_null($carry)){
            return "CARRY is null. item:".$item;
        }
        
        if($carry instanceof Closure){
            dump($carry());
            return strtolower($item);
        }
        return $item;
    };
});
 
dump($res());

這個例子第二個參數回調函數返回的是一個閉包,也就是說 array_reduce 函數最終返回的也是一個閉包,除非執行這個閉包,否則里面的邏輯不會執行,這也是閉包的神奇之處,我們可以把函數“暫存”起來以后執行。

第一次迭代,$carry 是空的,返回一個字符串。

第二次迭代,因為第一次返回了一個閉包,所以這次 $carry 是一個閉包,返回小寫字母。

第三次迭代,因為第二次迭代返回的是一個閉包,所以也是返回一個小寫字母。

這個閉包的執行結果是:

"item:CCCC"
"item:BBBB"
"item:AAAA"
"CARRY is null. item:AAAA"
"bbbb"
"cccc"

一定要弄懂為什么這樣輸出,它的執行順序是反的,可以理解為每一次迭代,就是把閉包函數丟到一個棧里面,后進先出。

實現的核心

接下來要分析 $this->carry() 這個方法,它是中間件實現的核心。

   /**
     * Get a Closure that represents a slice of the application onion.
     *
     * @return \Closure
     */
    protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                try {
                    $slice = parent::carry();

                    $callable = $slice($stack, $pipe);

                    return $callable($passable);
                } catch (Exception $e) {
                    return $this->handleException($passable, $e);
                } catch (Throwable $e) {
                    return $this->handleException($passable, new FatalThrowableError($e));
                }
            };
        };
    }
/**
 * Get a Closure that represents a slice of the application onion.
 *
 * @return \Closure
 */
 protected function carry(){
    return function($stack, $pipe){
        return function($passable)use($stack, $pipe){
            if(is_callable($pipe)){
                // If the pipe is an instance of a Closure, we will just call it directly but
                // otherwise we'll resolve the pipes out of the container and call it with
                // the appropriate method and arguments, returning the results back out.
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)){
                list($name, $parameters)= $this->parsePipeString($pipe);
 
                // If the pipe is a string we will parse the string and resolve the class out
                // of the dependency injection container. We can then build a callable and
                // execute the pipe function giving in the parameters that are required.
                $pipe = $this->getContainer()->make($name);
 
                $parameters = array_merge([$passable, $stack], $parameters);
            }else{
                // If the pipe is already an object we'll just make a callable and pass it to
                // the pipe as-is. There is no need to do any extra parsing and formatting
                // since the object we're given was already a fully instantiated object.
                $parameters =[$passable, $stack];
            }
 
            return method_exists($pipe, $this->method)
                            ? $pipe->{$this->method}(...$parameters)
                            : $pipe(...$parameters);
        };
    };
}

這個 carry 方法返回一個閉包(或者說函數也可以),作為 array_reduce 的第二個參數作為回調函數。這個方法看上去很復雜,閉包里面返回閉包,但是搞清楚了之后就沒這么難。

這個作為 array_reduce 的回調函數的閉包,接受兩個參數,第一個參數也是個閉包,而且第一次迭代的閉包是另外一個方法提供的,第二個參數是中間件,是一個字符串形式。

第一次迭代,$stack 參數是 $this->dispatchToRouter() 返回的閉包,實際上放到最后執行了,$pipe 參數是 Illuminate\Routing\Middleware\SubstituteBindings,(注意 array_reverse 把 pipes 數組反轉了,實際上理解了原理就知道這樣做反而是要按中間件定義的順序執行),那么根據判斷邏輯,從容器中取出中間件,最后執行中間件的 handle 方法,並傳入 $request 和 $stack 作為參數, 但實際上並沒有任何實際的執行,注意這個函數返回的也是一個閉包。

第二次迭代,還是執行這個回調函數,此時 $stack 就變成了第一次也就是上次迭代返回的閉包了,第二個參數 $pipe 就是 App\Http\Middleware\VerifyCsrfToken,其他過程同上,也返回一個閉包。

……

最后一次迭代,$stack 是上一次返回的閉包,$pipe 就是 App\Http\Middleware\EncryptCookies,但到此沒有任何實際的執行,因為沒有調用。

這些閉包,可以理解為放到一個“棧”里面了,執行的時候從最外層開始往里面執行,后進先出。

最后,then 方法里 return $pipeline($this->passable) 才是調用 array_reduce 返回的最終的閉包,開始真正執行這些中間件了。

前置和后置中間件

我們把控制器方法改成:

Route::get('test',function(){
    dump('this is controller');
   //dump(debug_backtrace());
});

然后隨便找一個中間件在 $response = $next($request) 前后打印點內容:

執行,頁面上的輸出如圖:

徹底弄懂 Laravel 中間件原理

這是為什么呢?

$response = $next($request);

這里 $next 就是前文中的 $stack,執行這句的時候就會把所有中間件都執行完,然后別忘了前面說的第一個閉包是 $this->dispatchToRouter() 提供的,它會進入到控制器邏輯,然后再是執行每個中間件中 $response = $next($request) 接下來的邏輯。這也是前置中間件和后置中間件的原理。

要徹底弄懂 Laravel 中間件原理,還需要親自熟悉 array_reduce 方法和理解閉包的概念。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM