1. 程式人生 > 實用技巧 >Laravel核心解讀--中介軟體(Middleware)

Laravel核心解讀--中介軟體(Middleware)

中介軟體(Middleware)在Laravel中起著過濾進入應用的HTTP請求物件(Request)和完善離開應用的HTTP響應物件(Reponse)的作用, 而且可以通過應用多箇中間件來層層過濾請求、逐步完善相應。這樣就做到了程式的解耦,如果沒有中介軟體那麼我們必須在控制器中來完成這些步驟,這無疑會造成控制器的臃腫。

舉一個簡單的例子,在一個電商平臺上使用者既可以是一個普通使用者在平臺上購物也可以在開店後是一個賣家使用者,這兩種使用者的使用者體系往往都是一套,那麼在只有賣家使用者才能訪問的控制器裡我們只需要應用兩個中介軟體來完成賣家使用者的身份認證:

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

在auth中介軟體裡做了通用的使用者認證,成功後HTTP Request會走到merchant_auth中介軟體裡進行商家使用者資訊的認證,兩個中介軟體都通過後HTTP Request就能進入到要去的控制器方法中了。利用中介軟體,我們就能把這些認證程式碼抽離到對應的中介軟體中了,而且可以根據需求自由組合多箇中間件來對HTTP Request進行過濾。

再比如Laravel自動給所有路由應用的VerifyCsrfToken中介軟體,在HTTP Requst進入應用走過VerifyCsrfToken中介軟體時會驗證Token防止跨站請求偽造,在Http Response 離開應用前會給響應新增合適的Cookie。(laravel5.5開始CSRF中介軟體只自動應用到web路由上)

上面例子中過濾請求的叫前置中介軟體,完善響應的叫做後置中介軟體。用一張圖可以標示整個流程:

上面概述了下中介軟體在laravel中的角色,以及什麼型別的程式碼應該從控制器挪到中介軟體裡,至於如何定義和使用自己的laravel 中介軟體請參考官方文件

下面我們主要來看一下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)裡:

/**
 * 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 ( arrayarray , callablearray,callablecallback [, mixed $initial = NULL ] )

array_reduce() 將回調函式 callback 迭代地作用到 array 陣列中的每一個單元中,從而將陣列簡化為單一的值。

callback ( mixedcarry , mixedcarry,mixeditem )
carry
攜帶上次迭代裡的值; 如果本次迭代是第一次,那麼這個值是 initial。item 攜帶了本次迭代的值。

getInitialSlice方法,他的返回值是作為傳遞給callbakc函式的carry引數的初始值,這個值現在是一個閉包,我把getInitialSlice和Http Kernel的dispatchToRouter這兩個方法合併一下,現在carrygetInitialSliceHttpKerneldispatchToRouter在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時它的值是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(請求物件),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執行完成後就會返回一個嵌套了多層閉包的閉包,每層閉包用到的外部變數stackcallback調arrayreduce量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原始碼學習裡,歡迎訪問閱讀。

轉載:https://segmentfault.com/a/1190000013154423