Laravel中介軟體(Middleware)的解讀

來源:互聯網
上載者:User
這篇文章主要介紹了關於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 ( 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是如何設計中介軟體的以及如何執行它們的,希望能對感興趣的朋友有協助。

以上就是本文的全部內容,希望對大家的學習有所協助,更多相關內容請關注topic.alibabacloud.com!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.