生命周期 7--如何根据 route 对象的信息获取路由和控制器中间件
前言
上一节已经分析了《一个request是如何匹配到具体路由的》,本文分析一下「如何根据route对象的信息获取路由和控制器中间件」,以便后面用来过滤处理request对象。
代码位置
在route对象的runRouteWithinStack方法中,\Illuminate\Routing\Router::runRouteWithinStack
。
/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
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()
);
});
}
要分析的代码就这么一句:
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
几个对象
- route 当前匹配路由对象
- router 路由器
具体分析
首先看是否绑定过middleware.disable的对象,很明显没有过。
然后执行$this->gatherRouteMiddleware($route)
/**
* Gather the middleware for the given route with resolved class names.
*
* @param \Illuminate\Routing\Route $route
* @return array
*/
public function gatherRouteMiddleware(Route $route)
{
//获取路由中间件和控制器中间件
//对上述获得的中间件集合遍历,将中间件的名称解析为对应的全类名为新集合
$middleware = collect($route->gatherMiddleware())->map(function ($name) {
return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
})->flatten();
//按照app/http/kernel文件中的middlewarePriority属性顺序来排序新集合并返回
return $this->sortMiddleware($middleware);
}
获取路由中间件
$route->gatherMiddleware()
/**
* Get all middleware, including the ones from the controller.
*
* @return array
*/
public function gatherMiddleware()
{
//如果当前route对象已经有computedMiddleware就直接返回
if (! is_null($this->computedMiddleware)) {
return $this->computedMiddleware;
}
$this->computedMiddleware = [];
//获取路由器中间件及控制器中间件,合并并去重,
//赋给route对象的computedMiddleware属性
return $this->computedMiddleware = array_unique(array_merge(
$this->middleware(), $this->controllerMiddleware()
), SORT_REGULAR);
}
$this->middleware()
/**
* Get or set the middlewares attached to the route.
*
* @param array|string|null $middleware
* @return $this|array
*/
public function middleware($middleware = null)
{
//如果传入为null,则返回当前route对象的action['middelware'],
if (is_null($middleware)) {
return (array) ($this->action['middleware'] ?? []);
}
//如果是字符串,则将传入的1到多个字符串转换为数组
if (is_string($middleware)) {
$middleware = func_get_args();
}
//将现有route对象的action['middelware']属性和传入的$middleware字符串
//合并,并赋给现有route对象的action['middelware']属性
$this->action['middleware'] = array_merge(
(array) ($this->action['middleware'] ?? []), $middleware
);
return $this;
}
上面的代码再分析一下:$this->action['middleware']
一般为web,因为每个请求都默认了web中间件组,在\App\Providers\RouteServiceProvider::mapWebRoutes
中有定义:
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::middleware('web') //可以看到这里就是上面代码中传入web字符串的情况
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
获取控制器中间件
$this->controllerMiddleware();
.
.
.
/**
* Get the middleware for the route's controller.
*
* @return array
*/
public function controllerMiddleware()
{
//如果当前路由的action不是控制器方法,就直接返回[]
if (! $this->isControllerAction()) {
return [];
}
//使用route的controllerDispatcher的getMiddleware方法去获取中间件
//传入的参数是控制器实例和控制器方法。
return $this->controllerDispatcher()->getMiddleware(
$this->getController(), $this->getControllerMethod()
);
}
注意,上面在传入控制器实例的参数时,这个控制器是解析出来的。会去执行控制器的__construct()方法,比如我这个路由uri是/login,对应的控制器是app/Http/Controllers/Auth/LoginController
。
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
//给当前控制器注册guest这个中间件,并设置其logout属性
$this->middleware('guest')->except('logout');
}
注册guest这个中间件
/**
* Register middleware on the controller.
*
* @param array|string|\Closure $middleware
* @param array $options
* @return \Illuminate\Routing\ControllerMiddlewareOptions
*/
public function middleware($middleware, array $options = [])
{
//对控制器构造函数中传入的中间件数组化后遍历注册到控制器
foreach ((array) $middleware as $m) {
$this->middleware[] = [
'middleware' => $m,
'options' => &$options,
];
}
//实例化一个ControllerMiddlewareOptions对象,
//便于后面使用其中的only,except方法。
return new ControllerMiddlewareOptions($options);
}
only,except我在最开始学习时,感觉到非常绕,文档也含糊,看了源码就明白了,无非就是设置options属性,方法如下:
/**
* Set the controller methods the middleware should apply to.
*
* @param array|string|dynamic $methods
* @return $this
*/
public function only($methods)
{
$this->options['only'] = is_array($methods) ? $methods : func_get_args();
return $this;
}
/**
* Set the controller methods the middleware should exclude.
*
* @param array|string|dynamic $methods
* @return $this
*/
public function except($methods)
{
$this->options['except'] = is_array($methods) ? $methods : func_get_args();
return $this;
}
但这里需要注意的是:$this->options
这里的$this
是一个ControllerMiddlewareOptions对象,而我们要设置的是中间件的options,二者是如何联系的呢?
首先,注册控制器的中间件的最后,会实例化一个ControllerMiddlewareOptions对象,而ControllerMiddlewareOptions对象的构造函数,是将options参数以引用方式进行传递的,并赋给了自己的options属性,这样设置ControllerMiddlewareOptions对象的options属性,就是设置中间件的options属性。操作异常的骚气呀。。
/**
* Create a new middleware option instance.
*
* @param array $options
* @return void
*/
public function __construct(array &$options)
{
$this->options = &$options;
}
好了,回到route的getController方法,返回刚才创建的控制器到controllerDispatcher对象的getMiddleware方法中作为第一参数,然后解析出控制器的方法作为第二参数。
下面就是获取控制器的中间件。
/**
* Get the middleware for the controller instance.
*
* @param \Illuminate\Routing\Controller $controller
* @param string $method
* @return array
*/
public function getMiddleware($controller, $method)
{
//如果指定控制器没有getMiddleware方法,则返回[]
if (! method_exists($controller, 'getMiddleware')) {
return [];
}
//根据返回的控制器中间件实例化一个集合,遍历,
//根据传入的这个方法判断某个中间件是否应该被排除。
return collect($controller->getMiddleware())->reject(function ($data) use ($method) {
return static::methodExcludedByOptions($method, $data['options']);
})->pluck('middleware')->all();
}
判断排除的方法如下:
/**
* Determine if the given options exclude a particular method.
*
* @param string $method
* @param array $options
* @return bool
*/
protected static function methodExcludedByOptions($method, array $options)
{
//如果是only属性,则看当前方法是否不在其中,如果不在,就返回true
return (isset($options['only']) && ! in_array($method, (array) $options['only'])) ||
//如果是except属性,则看当前方法是否在其中,如果在,就返回true
(! empty($options['except']) && in_array($method, (array) $options['except']));
}
上面的方法只要返回true,那么对于的中间件就会被排除。
由于我当前的方法为showLoginForm,不在排除的属性中,因此guest中间件是有效的。
解析全类名中间件
前面只是获取了两个中间件的标识:web
和guest
,因此还需要将其解析为中间件的全类名。调用router对象的MiddlewareNameResolver::resolve
方法。
(array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups)
注意:这里的$this->middleware
是中间件别名数组,包括了\App\Http\Kernel::$routeMiddleware
中设置的别名和第三方包设置的别名。比如我现在这个数组为:
array (
'auth' => 'App\\Http\\Middleware\\Authenticate',
'auth.basic' => 'Illuminate\\Auth\\Middleware\\AuthenticateWithBasicAuth',
'bindings' => 'Illuminate\\Routing\\Middleware\\SubstituteBindings',
'cache.headers' => 'Illuminate\\Http\\Middleware\\SetCacheHeaders',
'can' => 'Illuminate\\Auth\\Middleware\\Authorize',
'guest' => 'App\\Http\\Middleware\\RedirectIfAuthenticated',
'signed' => 'Illuminate\\Routing\\Middleware\\ValidateSignature',
'throttle' => 'Illuminate\\Routing\\Middleware\\ThrottleRequests',
'verified' => 'Illuminate\\Auth\\Middleware\\EnsureEmailIsVerified',
'api.auth' => 'Dingo\\Api\\Http\\Middleware\\Auth',
'api.throttle' => 'Dingo\\Api\\Http\\Middleware\\RateLimit',
'api.controllers' => 'Dingo\\Api\\Http\\Middleware\\PrepareController',
'serializer' => 'Liyu\\Dingo\\SerializerSwitch',
'jwt.auth' => 'Tymon\\JWTAuth\\Http\\Middleware\\Authenticate',
'jwt.check' => 'Tymon\\JWTAuth\\Http\\Middleware\\Check',
'jwt.refresh' => 'Tymon\\JWTAuth\\Http\\Middleware\\RefreshToken',
'jwt.renew' => 'Tymon\\JWTAuth\\Http\\Middleware\\AuthenticateAndRenew',
)
可以看到后面几个都是第三方包设置的别名中间件:
'api.auth' => 'Dingo\\Api\\Http\\Middleware\\Auth',
'api.throttle' => 'Dingo\\Api\\Http\\Middleware\\RateLimit',
'api.controllers' => 'Dingo\\Api\\Http\\Middleware\\PrepareController',
'serializer' => 'Liyu\\Dingo\\SerializerSwitch',
'jwt.auth' => 'Tymon\\JWTAuth\\Http\\Middleware\\Authenticate',
'jwt.check' => 'Tymon\\JWTAuth\\Http\\Middleware\\Check',
'jwt.refresh' => 'Tymon\\JWTAuth\\Http\\Middleware\\RefreshToken',
'jwt.renew' => 'Tymon\\JWTAuth\\Http\\Middleware\\AuthenticateAndRenew',
比如Dingo,就可以在vendor/dingo/api/src/Provider/LaravelServiceProvider
的boot方法中找到下面这段:
$this->addMiddlewareAlias('api.auth', Auth::class);
$this->addMiddlewareAlias('api.throttle', RateLimit::class);
$this->addMiddlewareAlias('api.controllers', PrepareController::class);
其他的都是一个意思,就不多说了。
而这里的$this->middlewareGroups
就是\App\Http\Kernel::$routeMiddleware
中设置的中间件组。
array (
'web' =>
array (
0 => 'App\\Http\\Middleware\\EncryptCookies',
1 => 'Illuminate\\Routing\\Middleware\\SubstituteBindings',
2 => 'Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse',
3 => 'Illuminate\\Session\\Middleware\\StartSession',
4 => 'Illuminate\\View\\Middleware\\ShareErrorsFromSession',
5 => 'App\\Http\\Middleware\\VerifyCsrfToken',
6 => 'App\\Http\\Middleware\\RecordLastActivedTime',
),
'api' =>
array (
0 => 'throttle:60,1',
1 => 'bindings',
),
)
然后开始将上面的web
和guest
解析为全类名,代码都很简单。
/**
* Resolve the middleware name to a class name(s) preserving passed parameters.
*
* @param string $name
* @param array $map
* @param array $middlewareGroups
* @return \Closure|string|array
*/
public static function resolve($name, $map, $middlewareGroups)
{
// When the middleware is simply a Closure, we will return this Closure instance
// directly so that Closures can be registered as middleware inline, which is
// convenient on occasions when the developers are experimenting with them.
//如果是闭包,直接返回。
if ($name instanceof Closure) {
return $name;
}
//如果是在别名中间件数组中的闭包,还是直接返回
if (isset($map[$name]) && $map[$name] instanceof Closure) {
return $map[$name];
}
// If the middleware is the name of a middleware group, we will return the array
// of middlewares that belong to the group. This allows developers to group a
// set of middleware under single keys that can be conveniently referenced.
//如果是在中间件组中,比如web,就去解析这个中间件组
if (isset($middlewareGroups[$name])) {
return static::parseMiddlewareGroup($name, $map, $middlewareGroups);
}
// Finally, when the middleware is simply a string mapped to a class name the
// middleware name will get parsed into the full class name and parameters
// which may be run using the Pipeline which accepts this string format.
//去掉中间件名字中的:
[$name, $parameters] = array_pad(explode(':', $name, 2), 2, null);
//如果从别名中间件中解析出了全类名就返回,不然就返回自身,当然还带上已有的参数。
return ($map[$name] ?? $name).(! is_null($parameters) ? ':'.$parameters : '');
}
/**
* Parse the middleware group and format it for usage.
*
* @param string $name
* @param array $map
* @param array $middlewareGroups
* @return array
*/
protected static function parseMiddlewareGroup($name, $map, $middlewareGroups)
{
$results = [];
//遍历中间件组,比如web组,就有多个中间件
foreach ($middlewareGroups[$name] as $middleware) {
// If the middleware is another middleware group we will pull in the group and
// merge its middleware into the results. This allows groups to conveniently
// reference other groups without needing to repeat all their middlewares.
//如果中间件还在另一个中间件组中,就将其取出来并合并到结果中
if (isset($middlewareGroups[$middleware])) {
$results = array_merge($results, static::parseMiddlewareGroup(
$middleware, $map, $middlewareGroups
));
continue;
}
[$middleware, $parameters] = array_pad(
explode(':', $middleware, 2), 2, null
);
// If this middleware is actually a route middleware, we will extract the full
// class name out of the middleware list now. Then we'll add the parameters
// back onto this class' name so the pipeline will properly extract them.
//如果是路由别名中间件组中的,那么就将其全类名取出。
if (isset($map[$middleware])) {
$middleware = $map[$middleware];
}
$results[] = $middleware.($parameters ? ':'.$parameters : '');
}
return $results;
}
}
这样,得到了一个全类名中间件集合,内容为:
array (
0 => 'App\\Http\\Middleware\\EncryptCookies',
1 => 'Illuminate\\Routing\\Middleware\\SubstituteBindings',
2 => 'Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse',
3 => 'Illuminate\\Session\\Middleware\\StartSession',
4 => 'Illuminate\\View\\Middleware\\ShareErrorsFromSession',
5 => 'App\\Http\\Middleware\\VerifyCsrfToken',
6 => 'App\\Http\\Middleware\\RecordLastActivedTime',
7 => 'App\\Http\\Middleware\\RedirectIfAuthenticated',
)
中间件排序
然后,就是根据router对象的middlewarePriority
数组中的优先级对上面这个中间件集合进行排序。
$this->sortMiddleware($middleware)
/**
* Sort the given middleware by priority.
*
* @param \Illuminate\Support\Collection $middlewares
* @return array
*/
protected function sortMiddleware(Collection $middlewares)
{
//创建一个SortedMiddleware对象并排序。
return (new SortedMiddleware($this->middlewarePriority, $middlewares))->all();
}
class SortedMiddleware extends Collection
{
/**
* Create a new Sorted Middleware container.
*
* @param array $priorityMap
* @param array|\Illuminate\Support\Collection $middlewares
* @return void
*/
public function __construct(array $priorityMap, $middlewares)
{
//如果是集合,则转换为数组
if ($middlewares instanceof Collection) {
$middlewares = $middlewares->all();
}
//开始优先级排序
$this->items = $this->sortMiddleware($priorityMap, $middlewares);
}
/**
* Sort the middlewares by the given priority map.
*
* Each call to this method makes one discrete middleware movement if necessary.
*
* @param array $priorityMap
* @param array $middlewares
* @return array
*/
protected function sortMiddleware($priorityMap, $middlewares)
{
$lastIndex = 0;
//遍历每个中间件
foreach ($middlewares as $index => $middleware) {
if (! is_string($middleware)) {
continue;
}
$stripped = head(explode(':', $middleware));
//当中间件在优先级数组中才分析
if (in_array($stripped, $priorityMap)) {
$priorityIndex = array_search($stripped, $priorityMap);
// This middleware is in the priority map. If we have encountered another middleware
// that was also in the priority map and was at a lower priority than the current
// middleware, we will move this middleware to be above the previous encounter.
//如果在优先级数组中,且当前优先级小于上一次设置的优先级,
//那么需要运用moveMiddleware方法将当前中间件移动到上一次中间件设置的位置
//然后再重新以这个新的集合重新进行优先级排序。
if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) {
return $this->sortMiddleware(
$priorityMap, array_values($this->moveMiddleware($middlewares, $index, $lastIndex))
);
}
// This middleware is in the priority map; but, this is the first middleware we have
// encountered from the map thus far. We'll save its current index plus its index
// from the priority map so we can compare against them on the next iterations.
//设置每次循环中间件的index和优先级index,便于比较。
$lastIndex = $index;
$lastPriorityIndex = $priorityIndex;
}
}
//返回去重后的数组。
return array_values(array_unique($middlewares, SORT_REGULAR));
}
/**
* Splice a middleware into a new position and remove the old entry.
*
* @param array $middlewares
* @param int $from
* @param int $to
* @return array
*/
protected function moveMiddleware($middlewares, $from, $to)
{
//将中间件的副本移动到指定位置
array_splice($middlewares, $to, 0, $middlewares[$from]);
//将原来位置的中间件清除
unset($middlewares[$from + 1]);
return $middlewares;
}
}
最后复习一下
上面router对象中的middlewarePriority
、middlewareGroups
, middleware
,我们都是直接拿来就用的,什么时候设置的?
这个是在创建kernel对象时就已经设置了,不得不佩服框架设计的精妙。
/**
* Create a new HTTP kernel instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$router->middlewarePriority = $this->middlewarePriority;
foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}
小结
- 在理解了生命周期后,源码的分析变得越来越简单。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: