中间件(Middleware)在Laravel中起到过滤进入应用的HTTP请求对象(Request)和改进离开应用的HTTP响应对象(Reponse)的作用,可以通过Apply多个中间件层层过滤请求,逐步完善响应。这样就实现了程序的解耦。如果没有中间件,我们必须在controller中完成这些步骤,这无疑会造成controller的臃肿。举个简单的例子,在电商平台上,用户既可以是在平台购物的普通用户,也可以是开店后的卖家用户。这两类用户的用户系统往往是相同的。在只允许用户访问的controller中,我们只需要应用两个中间件就可以完成对商户用户的认证:$this->middleware('mechatnt_auth');}}在auth中间件中做一般的用户认证。成功后HTTPRequest会去merchant_auth中间件对商户用户信息进行认证。两个中间件都通过后,HTTPRequest就可以进入你想去的controller方法了。使用中间件,我们可以将这些认证码提取到相应的中间件中,并可以根据需要自由组合多个中间件来过滤HTTPRequest。另一个例子是Laravel自动将VerifyCsrfToken中间件应用于所有路由应用程序。当HTTPRequest进入应用程序,经过VerifyCsrfToken中间件时,会对Token进行校验,防止跨站请求伪造。在HttpResponse离开应用程序之前,它会在响应中添加一个合适的cookie。(从laravel5.5开始,CSRF中间件只自动应用到web路由上。)在上面的例子中,过滤请求的称为前置中间件,改善响应的称为后置中间件。整个过程可以用一张图来标记一下:上面概括了中间件在laravel中的作用,什么类型的代码应该从controller移到middleware中。至于如何定义和使用自己的laravel中间件,请参考官方文档。接下来,我们就来看看如何在Laravel中实现中间件。中间件的设计使用了一种称为装饰器的设计模式。如果不知道什么是装饰者模式,可以参考设计模式相关的书籍,或者直接参考这篇文章。Laravel实例化Application后,会从服务容器中解析出HttpKernel对象。从类名可以看出HttpKernel是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中可以看到,HttpKernel是从服务容器中解析出来的,因为在bootstrap/app.php中绑定了Illuminate\Contracts\Http\Kernel接口的实现类App\Http\Kernel,$kernel是实际上是App\Http\Kernel类的一个对象。Laravel在解析完HttpKernel之后,将进入应用程序的请求对象传递给HttpKernel的handle方法。handle方法负责处理流入应用程序的请求对象并返回响应对象。/***处理传入的HTTP请求。**@param\Illuminate\Http\Request$request*@return\Illuminate\Http\Response*/publicfunctionhandle($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=newFatalThrowableError($e));$response=$this->renderException($request,$e);}$this->app['events']->dispatch(newEvents\RequestHandled($request,$response));return$response;}中间件过滤应用的过程就发生在$this->sendRequestThroughRouter($request)里:/***通过中间件/路由器发送给定的请求。**@param\Illuminate\Http\Request$request*@return\Illuminate\Http\Response*/protected函数sendRequestThroughRouter($request){$this->app->instance('request',$request);Facade::clearResolvedInstance('request');$this->bootstrap();返回(新管道($this->app))->send($request)->through($this->app->shouldSkipMiddleware()?[]:$this->middleware)->then($this->dispatchToRouter());}这个方法的前半部分是初始化Application。在之前讲解服务提供者的文章中,有这部分的详细讲解。Laravel通过Pipeline(管道)对象传输请求对象。在Pipeline中,request对象依次经过HttpKernel中定义的中间层。组件的预操作达到控制器的某个动作或者直接关闭处理得到响应对象。查看Pipeline中的这些方法:publicfunctionsend($passable){$this->passable=$passable;返回$this;}公共函数通过($pipes){$this->pipes=is_array($pipes)?$管道:func_get_args();返回$this;}publicfunctionthen(Closure$destination){$firstSlice=$this->getInitialSlice($destination);//pipes是要传递的中间件$pipes=array_reverse($this->pipes);//$this->passable是Request对象($passable)使用($destination){returncall_user_func($destination,$passable);};}//HttpKernel的dispatchToRouter是Piple管道的末端或目的地protectedfunctiondispatchToRouter(){returnfunction($request){$this->app->instance('request',$request);返回$this->router->dispatch($request);};}上面的函数看着头晕,我们先我们来看看array_reduce中对其回调函数参数的解释:mixedarray_reduce(array$array,callable$callback[,mixed$initial=NULL])array_reduce()将回调函数callback迭代地应用于每个单元,从而简化了array到单个值回调(mixed$carry,mixed$item)carry携带上一次迭代的值;如果此迭代是第一次,则此值为初始值。item携带本次迭代的值。getInitialSlice方法,其返回值作为传递给callbakc函数的$carry参数的初始值,这个值现在是一个闭包,我结合了getInitialSlice和HttpKernel的dispatchToRouter这两个方法,现在$firstSlice的值是:$destination=function($request){$this->app->instance('request',$request);返回$this->router->dispatch($request);};$firstSlice=function($passable)use($destination){returncall_user_func($destination,$passable);};接下来看array_reduce的callback://PipelineprotectedfunctiongetSlice(){returnfunction($stack,$pipe){returnfunction($passable)use($stack,$pipe){try{$slice=parent::获取切片();返回call_user_func($slice($stack,$pipe),$passable);}catch(Exception$e){return$this->handleException($passable,$e);}catch(Throwable$e){return$this->handleException($passable,newFatalThrowableError($e));}};};}//Pipleline的父类BasePipe行的getSlice方法protectedfunctiongetSlice(){returnfunction($stack,$pipe){returnfunction($passable)use($stack,$pipe){if($pipeinstanceofClosure){returncall_user_func($pipe,$passable,$堆栈);}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=handlereturncall_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();返回call_user_func($slice($stack,$pipe),$passable);}catch(Exception$e){return$this->handleException($passable,$e);}catch(Throwable$e){return$this->handleException($passable,newFatalThrowableError($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方法中,onion闭包会在array_reduce执行完毕并返回最终结果后调用:这样就可以依次执行中间件的handle方法,在handle方法中,会再次调用之前提到的reduce包的洋葱闭包剩余部分,这样一层层剥洋葱,直到结尾。这样,请求对象就依次流过要传递的中间件,到达目的HttpKernel的dispatchToRouter方法。通过剥洋葱的过程,我们可以知道为什么中间件数组要在array_reduce之前反转,因为打包是一个反转的过程,数组$pipes中的第一个中间件会被第一个reduce执行的结果包裹在洋葱闭包的最内层,所以只有反转后才能保证最先调用初始定义的中间件数组中第一个中间件的handle方法。上面说了Pipeline传输request对象的目的地是HttpKernel的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,];当request对象进入HttpKernel的dispatchToRouter方法时,当request对象被Routerdispatch派发到路由上时,路由上应用的中间件和controller中应用的中间件都会被收集起来。命名空间Illuminate\Foundation\Http;classKernelimplementsKernelContract{protectedfunctiondispatchToRouter(){returnfunction($request){$this->app->instance('request',$request);返回$this->router->dispatch($request);};}}namespaceIlluminate\Routing;classRouter实现RegistrarContract,BindingRegistrar{publicfunctiondispatch(Request$request){$this->currentRequest=$request;返回$this->dispatchToRoute($request);}publicfunctiondispatchToRoute(Request$request){return$this->runRoute($request,$this->findRoute($request));}protectedfunctionrunRoute(Request$request,Route$route){$request->setRouteResolver(function()use($route){return$route;});$this->events->dispatch(newEvents\RouteMatched($route,$request));返回$this->prepareResponse($request,$this->runRouteWithinStack($route,$request));}protectedfunctionrunRouteWithinStack(Route$route,Request$request){$shouldSkipMiddleware=$this->container->bound('middleware.disable')&&$this->container->make('middleware.disable')===真;//收集应用在路由和控制器中的中间件$middleware=$shouldSkipMiddleware?[]:$this->gatherRouteMiddleware($路由);return(newPipeline($this->container))->send($request)->through($middleware)->then(function($request)使用($route){return$this->prepareResponse($request,$route->run());});}}收集完路由和控制器中应用的中间件后,仍然使用Pipeline对象通过收集到的中间件传递请求对象,然后到达最终目的地,会执行路由对应的控制器方法产生响应对象,然后响应对象将依次经过上面应用的所有中间件的后操作,最终离开申请后将申请发送给客户端。限于篇幅和文章的可读性,收集路由和controller中间件然后执行路由对应的处理方法的过程我就不详述了。感兴趣的同学可以自行阅读。Router的源码,本文的目的主要是梳理一下laravel是如何设计中间件以及如何执行的,希望对感兴趣的朋友有所帮助。本文已收录在Laravel源码学习系列文章中,欢迎访问阅读。
