生命周期 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中间件是有效的。

解析全类名中间件

前面只是获取了两个中间件的标识:webguest,因此还需要将其解析为中间件的全类名。调用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',
  ),
)

然后开始将上面的webguest解析为全类名,代码都很简单。

    /**
     * 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对象中的middlewarePrioritymiddlewareGroups, 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 协议》,转载必须注明作者和本文链接
日拱一卒
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 2

你为何如此优秀

6年前 评论

这篇写得比较清楚,谢谢。

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
92
粉丝
87
喜欢
152
收藏
121
排名:72
访问:11.3 万
私信
所有博文
社区赞助商