中間件(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 , callablearray,callablecallback [, mixed $initial = NULL ] )array_reduce() 將回調函數 callback 迭代地作用到 array 數組中的每一個單元中,從而將數組簡化為單一的值。
callback ( mixed carry , mixedcarry,mixeditem )
carry
攜帶上次迭代里的值; 如果本次迭代是第一次,那么這個值是 initial。item 攜帶了本次迭代的值。
getInitialSlice方法,他的返回值是作為傳遞給callbakc函數的carry參數的初始值,這個值現在是一個閉包,我把getInitialSlice和Http Kernel的dispatchToRouter這兩個方法合並一下,現在carry參數的初始值,這個值現在是一個閉包,我把getInitialSlice和HttpKernel的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時它的值是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執行完成后就會返回一個嵌套了多層閉包的閉包,每層閉包用到的外部變量stack傳遞給下一次對callback的調用,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