中间件(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中是怎么实现中间件的,中间件的设计应用了一种叫作装饰器的设计模式,若是你还不知道什么是装饰器模式能够查阅设计模式相关的书,也能够翻看我以前的文章装饰模式(DecoratorPattern)。设计模式
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方法负责处理流入应用的请求对象并返回响应对象。bash
/**
* 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
callback [, mixed $initial = NULL ] )
array_reduce() 将回调函数 callback 迭代地做用到 array 数组中的每个单元中,从而将数组简化为单一的值。
callback ( mixed
item ) carry 携带上次迭代里的值; 若是本次迭代是第一次,那么这个值是 initial。item 携带了本次迭代的值。
getInitialSlice方法,他的返回值是做为传递给callbakc函数的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会返回一个闭包函数, 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方法,他返回的也是一个闭包,在闭包会里解析出中间件对象、中间件参数(无则为空数组), 而后把stack和中间件参数做为中间件handle方法的参数进行调用。
上面封装的有点复杂,咱们简化一下,其实getSlice的返回值就是:
$stack = function ($passable) use ($stack, $pipe) {
//解析中间件和中间件参数,中间件参数用$parameter表明,无参数时为空数组
$parameters = array_merge([$passable, $stack], $parameters)
return $pipe->handle($parameters)
};
复制代码
array_reduce每次调用callback返回的闭包都会做为参数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()
);
});
}
}
namespace Illuminate\Routing;
class Route
{
public function run()
{
$this->container = $this->container ?: new Container;
try {
if ($this->isControllerAction()) {
return $this->runController();
}
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
}
复制代码
收集完路由和控制器里应用的中间件后,依然是利用Pipeline对象来传送请求对象经过收集上来的这些中间件而后到达最终的目的地,在那里会执行目的路由的run方法,run方法里面会判断路由对应的是一个控制器方法仍是闭包而后进行相应地调用,最后把执行结果包装成Response对象。Response对象会依次经过上面应用的全部中间件的后置操做,最终离开应用被发送给客户端。
限于篇幅和为了文章的可读性,收集路由和控制器中间件而后执行路由对应的处理方法的过程我就不在这里详述了,感兴趣的同窗能够本身去看Router的源码,本文的目的仍是主要为了梳理laravel是如何设计中间件的以及如何执行它们的,但愿能对感兴趣的朋友有帮助。
本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。