生命周期 6--一个 request 是如何匹配到具体路由的源码分析

导语

前面已经分析了request对象是如何通过全局中间件过滤后到达路由器

接下来的大致步骤

  • request是如何匹配具体路由。
  • 如何根据request对象的信息获取路由和控制器中间件的。
  • 如何穿透这些中间件的。
  • 穿过后,是如何进入控制器与应用程序交互的(一般写程序只写这么一点。。)。
  • 交互后,返回结果又是如何再次通过路由和控制器中间件和全局中间件的。
  • 最后的得到response对象。

接下来的书写风格

  • 限于时间关系,只说大致思路,不会深入太多。
  • 不解释属性含义,代码中有,自行查阅即可。
  • 不严格区分方法是从子类和父类执行,除非子类和父类中有相同方法。
  • 能够通过注释讲清楚的,优先写注释。
  • 方法所属命名空间,不影响理解的都会忽略,自行查阅或debug即可。

几个英文单词

  • route: 单个路由
  • routes: 多个路由(RouteCollection)
  • router: 路由器(放置routes的容器)

如何匹配路由

代码分析起点选择在vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:176,你我的可能略有差异,如下:

$this->router->dispatch($request);

上面这句之所以没有写上return,是因为限于篇幅和时间限制,本文远远分析不到执行return的地方。

dispatch方法如下:

    /**
     * Dispatch the request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
     */
    public function dispatch(Request $request)
    {
        //使用request对象设置kernel的currentRequest属性
        $this->currentRequest = $request;   

        //将request发送到一个路由(哪个?)并返回response
        return $this->dispatchToRoute($request);
    }

dispatchToRoute方法如下:

    /**
     * Dispatch the request to a route and return the response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    public function dispatchToRoute(Request $request)
    {
        //通过request对象找到一个匹配的路由,并执行此路由并返回结果。
        return $this->runRoute($request, $this->findRoute($request));
    }

本文重点来了,如何找到路由

$this->findRoute($request)

findRoute方法如下:

    /**
     * Find the route matching a given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Routing\Route
     */
    protected function findRoute($request)
    {
        //找到匹配request的第一个路由,如果找不到抛出NotFoundHttpException
        $this->current = $route = $this->routes->match($request);

        //设置route对象缓存
        $this->container->instance(Route::class, $route);

        //返回路由供后面run
        return $route;
    }

match方法如下:

    /**
     * Find the first route matching a given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Routing\Route
     *
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    public function match(Request $request)
    {
        //通过request的method来获取router中的该方法类型的routes
        $routes = $this->get($request->getMethod());

        // First, we will see if we can find a matching route for this current request
        // method. If we can, great, we can just return it so that it can be called
        // by the consumer. Otherwise we will check for routes with another verb.

        //从上述的routes中,再通过route对象的matches方法来获取匹配
        //request的具体路由route
        $route = $this->matchAgainstRoutes($routes, $request);

        //将获得的route绑定到request,并返回给findRoute方法。
        if (! is_null($route)) {
            return $route->bind($request);
        }

        // If no route was found we will now check if a matching route is specified by
        // another HTTP verb. If it is we will need to throw a MethodNotAllowed and
        // inform the user agent of which HTTP verb it should use for this route.

        //如果上面没找到对应route,那么会检查其他的HTTP动词动词
        $others = $this->checkForAlternateVerbs($request);

        if (count($others) > 0) {
            return $this->getRouteForMethods($request, $others);
        }

        //如果还找不到,就会抛出NotFoundHttpException
        throw new NotFoundHttpException;
    }

routes对象的matchAgainstRoutes方法就是神奇匹配的地方:

    /**
     * Determine if a route in the array matches the request.
     *
     * @param  array  $routes
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $includingMethod
     * @return \Illuminate\Routing\Route|null
     */
    protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
    {
        //将传入的routes按照是否有Fallback路由分为两部分。
        //注意是Fallback而非callback。。
        [$fallbacks, $routes] = collect($routes)->partition(function ($route) {
            return $route->isFallback;
        });

        //然后遍历合并后的routes,对每个route,都使用其matches方法来判断
        //request和路由是否完全匹配!
        return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
            return $value->matches($request, $includingMethod);
        });
    }

route对象的matches方法:

    /**
     * Determine if the route matches given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  bool  $includingMethod
     * @return bool
     */
    public function matches(Request $request, $includingMethod = true)
    {
        //将当前路由(route对象)编译为一个Symfony CompiledRoute instance
        //并赋值给route对象的compiled属性,后面会多次用到这个属性。
        $this->compileRoute();

        //然后是获取当前路由的validators,默认有4个。
        //UriValidator, MethodValidator,
        //SchemeValidator, HostValidator,
        //然后遍历验证器数组,每个验证器处理一个点。
        foreach ($this->getValidators() as $validator) {
            if (! $includingMethod && $validator instanceof MethodValidator) {
                continue;
            }

            //只要有一个验证器没有通过,返回false到前面的match方法中
            if (! $validator->matches($this, $request)) {
                return false;
            }
        }
        //全部通过,返回true
        return true;
    }

最后,将找到的route对象返回到dispatchToRoute方法中。

$this->runRoute($request, $this->findRoute($request))

下一步,似乎就可以runRoute了。

runRoute方法

    /**
     * Return the response for the given route.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Routing\Route  $route
     * @return mixed
     */
    protected function runRoute(Request $request, Route $route)
    {
        //设置request对象的路由解析器
        $request->setRouteResolver(function () use ($route) {
            return $route;
        });

        //触发注册过的路由匹配事件!
        $this->events->dispatch(new Events\RouteMatched($route, $request));

        return $this->prepareResponse($request,
            $this->runRouteWithinStack($route, $request)
        );
    }

触发注册过的路由匹配事件!

$this->events->dispatch(new Events\RouteMatched($route, $request))

默认是没有注册这个事件的,但可以在appServiceProvide的boot方法或自己的serviceProvider中的boot方法中自定义,比如在vendor/hieu-le/active/src/ActiveServiceProvider.php中。

    public function boot()
    {
        // Update the instances each time a request is resolved and a route is matched
        $instance = app('active');
        if (version_compare(Application::VERSION, '5.2.0', '>=')) {
            app('router')->matched(  //就是这里
                function (RouteMatched $event) use ($instance) {
                    $instance->updateInstances($event->route, $event->request);
                }
            );
        } else {
            app('router')->matched(
                function ($route, $request) use ($instance) {
                    $instance->updateInstances($route, $request);
                }
            );
        }
    }

然后就是如何根据request对象的信息获取路由和控制器中间件的(下一篇文章再写)

小结

  • serviceProvider的register方法都是用来注册类到serviceContainer中的或者起别名的。
  • 其他工作比如添加中间件,添加事件监听器,加载配置信息,环境变量,视图文件等等就放到boot方法中。毕竟有了类,才能使用其中的方法来干事情。
  • serviceContainer和各个组件之间往往通过互为属性的关系来达到数据共享。有时也可以通过明确引用传递参数的方式。
  • 以上代码分析都比较浅,如果深入研究下去,可以点亮更多技能,是否有用另说。
  • 闭包回调非常重要,几乎所有的难点都集中在这上面。
  • 看源码就是在学PHP的各种高级用法,开拓思路,同时理解了框架原理,以后用起来会得心应手。
  • 读源码时,先看接口方法,再看实现类中的属性和方法,然后再看执行源码过程。执行源码时,先看是哪个对象,然后执行哪个方法或者赋值给哪个属性。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 1

写的相当精彩。尤其最后总结中这句 “看源码就是在学PHP的各种高级用法,开拓思路,同时理解了框架原理,以后用起来会得心应手”,相当精妙。

8个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!