Laravel核心解讀--中間件(Middleware)

中間件(Middleware)在Laravel中起着過濾進入應用的HTTP請求對象(Request)和完善離開應用的HTTP響應對象(Reponse)的做用, 並且能夠經過應用多箇中間件來層層過濾請求、逐步完善相應。這樣就作到了程序的解耦,若是沒有中間件那麼咱們必須在控制器中來完成這些步驟,這無疑會形成控制器的臃腫。php

舉一個簡單的例子,在一個電商平臺上用戶既能夠是一個普通用戶在平臺上購物也能夠在開店後是一個賣家用戶,這兩種用戶的用戶體系每每都是一套,那麼在只有賣家用戶才能訪問的控制器裏咱們只須要應用兩個中間件來完成賣家用戶的身份認證:laravel

class MerchantController extends Controller$
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('mechatnt_auth');
    }
}

在auth中間件裏作了通用的用戶認證,成功後HTTP Request會走到merchant_auth中間件裏進行商家用戶信息的認證,兩個中間件都經過後HTTP Request就能進入到要去的控制器方法中了。利用中間件,咱們就能把這些認證代碼抽離到對應的中間件中了,並且能夠根據需求自由組合多箇中間件來對HTTP Request進行過濾。git

再好比Laravel自動給全部路由應用的VerifyCsrfToken中間件,在HTTP Requst進入應用走過VerifyCsrfToken中間件時會驗證Token防止跨站請求僞造,在Http Response 離開應用前會給響應添加合適的Cookie。(laravel5.5開始CSRF中間件只自動應用到web路由上)github

上面例子中過濾請求的叫前置中間件,完善響應的叫作後置中間件。用一張圖能夠標示整個流程:
圖片描述web

上面概述了下中間件在laravel中的角色,以及什麼類型的代碼應該從控制器挪到中間件裏,至於如何定義和使用本身的laravel 中間件請參考官方文檔bootstrap

下面咱們主要來看一下Laravel中是怎麼實現中間件的,中間件的設計應用了一種叫作裝飾器的設計模式,若是你還不知道什麼是裝飾器模式能夠查閱設計模式相關的書,也能夠簡單參考下這篇文章設計模式

Laravel實例化Application後,會從服務容器裏解析出Http Kernel對象,經過類的名字也能看出來Http Kernel就是Laravel裏負責HTTP請求和響應的核心。數組

/**
 * @var \App\Http\Kernel $kernel
 */
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

index.php裏能夠看到,從服務容器裏解析出Http Kernel,由於在bootstrap/app.php裏綁定了Illuminate\Contracts\Http\Kernel接口的實現類App\Http\Kernel因此$kernel其實是App\Http\Kernel類的對象。
解析出Http Kernel後Laravel將進入應用的請求對象傳遞給Http Kernel的handle方法,在handle方法負責處理流入應用的請求對象並返回響應對象。閉包

/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
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);
    }

    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}

中間件過濾應用的過程就發生在$this->sendRequestThroughRouter($request)裏:app

/**
 * 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 (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

這個方法的前半部分是對Application進行了初始化,在上一篇講解服務提供器的文章裏有對這一部分的詳細講解。Laravel經過Pipeline(管道)對象來傳輸請求對象,在Pipeline中請求對象依次經過Http Kernel裏定義的中間件的前置操做到達控制器的某個action或者直接閉包處理獲得響應對象。

看下Pipeline裏這幾個方法:

public function send($passable)
{
    $this->passable = $passable;

    return $this;
}

public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();

    return $this;
}

public function then(Closure $destination)
{
    $firstSlice = $this->getInitialSlice($destination);
    
    //pipes 就是要經過的中間件
    $pipes = array_reverse($this->pipes);

    //$this->passable就是Request對象
    return call_user_func(
        array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
    );
}


protected function getInitialSlice(Closure $destination)
{
    return function ($passable) use ($destination) {
        return call_user_func($destination, $passable);
    };
}

//Http Kernel的dispatchToRouter是Piple管道的終點或者叫目的地
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);

        return $this->router->dispatch($request);
    };
}

上面的函數看起來比較暈,咱們先來看下array_reduce裏對它的callback函數參數的解釋:

mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )

array_reduce() 將回調函數 callback 迭代地做用到 array 數組中的每個單元中,從而將數組簡化爲單一的值。

callback ( mixed $carry , mixed $item )
carry
攜帶上次迭代裏的值; 若是本次迭代是第一次,那麼這個值是 initial。item 攜帶了本次迭代的值。

getInitialSlice方法,他的返回值是做爲傳遞給callbakc函數的$carry參數的初始值,這個值如今是一個閉包,我把getInitialSlice和Http Kernel的dispatchToRouter這兩個方法合併一下,如今$firstSlice的值爲:

$destination = function ($request) {
    $this->app->instance('request', $request);
    return $this->router->dispatch($request);
};

$firstSlice = function ($passable) use ($destination) {
    return call_user_func($destination, $passable);
};

接下來咱們看看array_reduce的callback:

//Pipeline 
protected function getSlice()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            try {
                $slice = parent::getSlice();

                return call_user_func($slice($stack, $pipe), $passable);
            } catch (Exception $e) {
                return $this->handleException($passable, $e);
            } catch (Throwable $e) {
                return $this->handleException($passable, new FatalThrowableError($e));
            }
        };
    };
}

//Pipleline的父類BasePipeline的getSlice方法
protected function getSlice()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if ($pipe instanceof Closure) {
                return call_user_func($pipe, $passable, $stack);
            } elseif (! is_object($pipe)) {
                //解析中間件名稱和參數 ('throttle:60,1')
                list($name, $parameters) = $this->parsePipeString($pipe);
                $pipe = $this->container->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else{
                $parameters = [$passable, $stack];
            }
            //$this->method = handle
            return call_user_func_array([$pipe, $this->method], $parameters);
        };
    };
}

注:在Laravel5.5版本里 getSlice這個方法的名稱換成了carry, 二者在邏輯上沒有區別,因此依然能夠參照着5.5版本里中間件的代碼來看本文。

getSlice會返回一個閉包函數, $stack在第一次調用getSlice時它的值是$firstSlice, 以後的調用中就它的值就是這裏返回的值個閉包了:

$stack = function ($passable) use ($stack, $pipe) {
            try {
                $slice = parent::getSlice();

                return call_user_func($slice($stack, $pipe), $passable);
            } catch (Exception $e) {
                return $this->handleException($passable, $e);
            } catch (Throwable $e) {
                return $this->handleException($passable, new FatalThrowableError($e));
            }
 };

getSlice返回的閉包裏又會去調用父類的getSlice方法,他返回的也是一個閉包,在閉包會裏解析出中間件對象、中間件參數(無則爲空數組), 而後把$passable(請求對象), $stack和中間件參數做爲中間件handle方法的參數進行調用。

上面封裝的有點複雜,咱們簡化一下,其實getSlice的返回值就是:

$stack = function ($passable) use ($stack, $pipe) {
                //解析中間件和中間件參數,中間件參數用$parameter表明,無參數時爲空數組
               $parameters = array_merge([$passable, $stack], $parameters)
               return $pipe->handle($parameters)
};

array_reduce每次調用callback返回的閉包都會做爲參數$stack傳遞給下一次對callback的調用,array_reduce執行完成後就會返回一個嵌套了多層閉包的閉包,每層閉包用到的外部變量$stack都是上一次以前執行reduce返回的閉包,至關於把中間件經過閉包層層包裹包成了一個洋蔥。

在then方法裏,等到array_reduce執行完返回最終結果後就會對這個洋蔥閉包進行調用:

return call_user_func( array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable);

這樣就能依次執行中間件handle方法,在handle方法裏又會去再次調用以前說的reduce包裝的洋蔥閉包剩餘的部分,這樣一層層的把洋蔥剝開直到最後。經過這種方式讓請求對象依次流過了要經過的中間件,達到目的地Http Kernel 的dispatchToRouter方法。

經過剝洋蔥的過程咱們就能知道爲何在array_reduce以前要先對middleware數組進行反轉, 由於包裝是一個反向的過程, 數組$pipes中的第一個中間件會做爲第一次reduce執行的結果被包裝在洋蔥閉包的最內層,因此只有反轉後才能保證初始定義的中間件數組中第一個中間件的handle方法會被最早調用。

上面說了Pipeline傳送請求對象的目的地是Http Kernel 的dispatchToRouter方法,其實到遠沒有到達最終的目的地,如今請求對象了只是剛經過了\App\Http\Kernel類裏$middleware屬性裏羅列出的幾個中間件:

protected $middleware = [
    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\TrustProxies::class,
];

當請求對象進入Http Kernel的dispatchToRouter方法後,請求對象在被Router dispatch派發給路由時會進行收集路由上應用的中間件和控制器裏應用的中間件。

namespace Illuminate\Foundation\Http;
class Kernel implements KernelContract
{
    protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);

            return $this->router->dispatch($request);
        };
    }
}


namespace Illuminate\Routing;
class Router implements RegistrarContract, BindingRegistrar
{    
    public function dispatch(Request $request)
    {
        $this->currentRequest = $request;

        return $this->dispatchToRoute($request);
    }
    
    public function dispatchToRoute(Request $request)
    {
        return $this->runRoute($request, $this->findRoute($request));
    }
    
    protected function runRoute(Request $request, Route $route)
    {
        $request->setRouteResolver(function () use ($route) {
            return $route;
        });

        $this->events->dispatch(new Events\RouteMatched($route, $request));

        return $this->prepareResponse($request,
            $this->runRouteWithinStack($route, $request)
        );
    }
    
    protected function runRouteWithinStack(Route $route, Request $request)
    {
        $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;
        //收集路由和控制器裏應用的中間件
        $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

        return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    });
    
    }
}

收集完路由和控制器裏應用的中間件後,依然是利用Pipeline對象來傳送請求對象經過收集上來的這些中間件而後到達最終的目的地,在那裏會執行路由對應的控制器方法生成響應對象,而後響應對象會依次來經過上面應用的全部中間件的後置操做,最終離開應用被髮送給客戶端。

限於篇幅和爲了文章的可讀性,收集路由和控制器中間件而後執行路由對應的處理方法的過程我就不在這裏詳述了,感興趣的同窗能夠本身去看Router的源碼,本文的目的仍是主要爲了梳理laravel是如何設計中間件的以及如何執行它們的,但願能對感興趣的朋友有幫助。

本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。

相關文章
相關標籤/搜索