Skip to main content

路由调度之路由匹配

简介

要实现浏览器 URL 定位到 Laravel 具体控制器方法,首先要做的工作就是路由匹配。

路由匹配,顾名思义:就是我们浏览器输入的 URL 与我们 Laravel 定义的路由能够一一对应,从而执行相应控制器方法,来实现我们的业务。

浏览器:

file

Laravel 路由

routes/web.php

//...

//
Route::get('/register', 'Auth\RegisterController@showRegistrationForm');

//...

本篇内容,我将对 Laravel 路由匹配具体实现方式,进行简要讲解

路由匹配实现步骤简述

路由匹配,主要以一步一步的层级调用来实现的。

  • 第一步,调用 Illuminate\Routing\Router 类 findRoute 方法,传入 Request 对象
  • 第二步,在 findRoute 方法中,调用 Illuminate\Routing\RouteCollection 类的 match 方法,传入 Request 对象
  • 第三步,在 match 方法中,根据 Request 对象记录的 HTTP 方法,提取对应 HTTP 方法的路由集
  • 第四步,在 match 方法中,调用 matchAgainstRoutes 方法,将缩小范围的路由集和 Request 对象传入
  • 第五步,在 matchAgainstRoutes 方法中利用集合的 partition 方法分离路由 isFallback 属性为 true 和 false 的两类路由集,然后将是 false 的放前面,是 true 的放后面
  • 第六步,在 matchAgainstRoutes 方法中利用集合的 first 方法匹配出第一个符合浏览器 URL 的路由,并返回。
  • 第七步,进入集合的 first 方法,可以知道,匹配方法主要调用了 Illuminate\Routing\Route 类的 matches 方法
  • 第八步,看一下 matches 方法,首先调用 compileRoute 方法,进行当前待匹配路由的编译工作
  • 第九步,在 compileRoute 方法,可以看到,先判断有没有编译过,没有编译过,则实例化 Illuminate\Routing\RouteCompiler 路由编译类,并调用它的 compile 方法
  • 第十步,在 compile 方法中,调用 getOptionalParameters 方法对 /user/{id} 这种带有变量的路由进行 {id} 或 {id?} 字符提取,然后,利用正则替换,删除 {id?} 中 ?
  • 第十一步,在 compile 方法中,实例化 Symfony\Component\Routing\Route Symfony 路由处理类,将 '/user/{id?}'['id' => null]、 id 的限制 where 条件传入,并调用 Symfony 路由类的 compile 方法
  • 第十二步,在 Symfony 路由类的 compile 方法中,首先判断有没有编译过路由,没有则直接调用 Symfony\Component\Routing\RouteCompiler Symfony 路由编译类的静态方法 compile
  • 第十三步,在 Symfony 路由编译类的 compile 方法中,执行路由编译,主要对路由的 uri 、uri 中的参数、http 请求 token、uri 待匹配的正则表达式进行相关转换和赋值,最后返回编译好的(实例化) Symfony\Component\Routing\CompiledRoute 类对象
  • 第十四步,将编译完的 Symfony\Component\Routing\CompiledRoute 类对象赋值到 Illuminate\Routing\Route 类的 compiled 属性上,然后我们从第八步继续往下说
  • 第十五步,分别实例化 UriValidator 路由 URI 匹配类、MethodValidator HTTP 匹配类、SchemeValidator HTTP 协议验证类、HostValidator 主机域名验证类这四个类
  • 第十六步,分别调用上一步实例化好四个类的 matches 方法,如果全都成功返回 true, 那么第七步的 first 方法成功匹配到了第一个路由,后面的将不进行匹配了
  • 第十七步,我们回到第二步,现在路由找到了,首先赋值到 Router 类的 current 属性,然后以 Route::class 类名为键,找到的路由对象为值,绑定到 Laravel 容器的 instances 属性上,方便后面执行控制器方法是调用
  • 第十八步,至此,路由匹配完成

路由匹配实现步骤代码

  • 第一步,调用 Illuminate\Routing\Router 类 findRoute 方法,传入 Request 对象

    public function dispatchToRoute(Request $request)
    {
    // $this->findRoute($request) 就是路由匹配的开始
    return $this->runRoute($request, $this->findRoute($request));
    }
  • 第二步,在 findRoute 方法中,调用 Illuminate\Routing\RouteCollection 类的 match 方法,传入 Request 对象

    protected function findRoute($request)
    {
    // 调用 `Illuminate\Routing\RouteCollection` 类的 match 方法
    $this->current = $route = $this->routes->match($request);

    // 将获取的路由绑定到 Laravel 容器中
    $this->container->instance(Route::class, $route);

    return $route;
    }
  • 第三步,在 match 方法中,根据 Request 对象记录的 HTTP 方法,提取对应 HTTP 方法的路由集

  • 第四步,在 match 方法中,调用 matchAgainstRoutes 方法,将缩小范围的路由集和 Request 对象传入

    public function match(Request $request)
    {
    // 在 match 方法中,根据 Request 对象记录的 HTTP 方法,提取对应 HTTP 方法的路由集
    $routes = $this->get($request->getMethod());

    // 在 match 方法中,调用 matchAgainstRoutes 方法,将缩小范围的路由集和 Request 对象传入
    $route = $this->matchAgainstRoutes($routes, $request);

    if (! is_null($route)) {
    return $route->bind($request);
    }

    $others = $this->checkForAlternateVerbs($request);

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

    throw new NotFoundHttpException;
    }
  • 第五步,在 matchAgainstRoutes 方法中利用集合的 partition 方法分离路由 isFallback 属性为 true 和 false 的两类路由集,然后将是 false 的放前面,是 true 的放后面

  • 第六步,在 matchAgainstRoutes 方法中利用集合的 first 方法匹配出第一个符合浏览器 URL 的路由,并返回。

  • 第七步,进入集合的 first 方法,可以知道,匹配方法主要调用了 Illuminate\Routing\Route 类的 matches 方法

    protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
    {
    // 在 matchAgainstRoutes 方法中利用集合的 partition 方法分离路由 isFallback 属性为 true 和 false 的两类路由集
    list($fallbacks, $routes) = collect($routes)->partition(function ($route) {
    return $route->isFallback;
    });

    // 将 isFallback 是 false 的放前面,是 true 的放后面,然后利用集合的 first 方法匹配出第一个符合浏览器 URL 的路由,并返回。
    return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
    // 进入集合的 first 方法,可以知道,匹配方法主要调用了 `Illuminate\Routing\Route` 类的 matches 方法
    return $value->matches($request, $includingMethod);
    });
    }
  • 第八步,看一下 matches 方法,首先调用 compileRoute 方法,进行当前待匹配路由的编译工作

    public function matches(Request $request, $includingMethod = true)
    {
    // 首先调用 compileRoute 方法,进行当前待匹配路由的编译工作
    $this->compileRoute();

    foreach ($this->getValidators() as $validator) {
    if (! $includingMethod && $validator instanceof MethodValidator) {
    continue;
    }

    if (! $validator->matches($this, $request)) {
    return false;
    }
    }

    return true;
    }
  • 第九步,在 compileRoute 方法,可以看到,先判断有没有编译过,没有编译过,则实例化 Illuminate\Routing\RouteCompiler 路由编译类,并调用它的 compile 方法

    protected function compileRoute()
    {
    // 先判断有没有编译过
    if (! $this->compiled) {
    // 没有编译过,则实例化 `Illuminate\Routing\RouteCompiler` 路由编译类,并调用它的 compile 方法
    $this->compiled = (new RouteCompiler($this))->compile();
    }

    return $this->compiled;
    }
  • 第十步,在 compile 方法中,调用 getOptionalParameters 方法对 /user/{id} 这种带有变量的路由进行 {id} 或 {id?} 字符提取,然后,利用正则替换,删除 {id?} 中 ?

  • 第十一步,在 compile 方法中,实例化 Symfony\Component\Routing\Route Symfony 路由处理类,将 '/user/{id?}'['id' => null]、 id 的限制 where 条件传入,并调用 Symfony 路由类的 compile 方法

    public function compile()
    {
    // 调用 getOptionalParameters 方法对 `/user/{id}` 这种带有变量的路由进行 {id} 或 {id?} 字符提取
    $optionals = $this->getOptionalParameters();

    // 利用正则替换,删除 {id?} 中 ?
    $uri = preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri());

    return (
    // 实例化 `Symfony\Component\Routing\Route` Symfony 路由处理类,将 `'/user/{id?}'`、`['id' => null]`、 id 的限制 where 条件传入,并调用 Symfony 路由类的 compile 方法
    new SymfonyRoute($uri, $optionals, $this->route->wheres, ['utf8' => true], $this->route->getDomain() ?: '')
    )->compile();
    }
    protected function getOptionalParameters()
    {
    // 实现 {id} 这种变量提取方式,主要借助正则表达式匹配
    preg_match_all('/\{(\w+?)\?\}/', $this->route->uri(), $matches);

    // array_fill_keys 方法定义了 ['id' => null] 带填充数组
    return isset($matches[1]) ? array_fill_keys($matches[1], null) : [];
    }
  • 第十二步,在 Symfony 路由类的 compile 方法中,首先判断有没有编译过路由,没有则直接调用 Symfony\Component\Routing\RouteCompiler Symfony 路由编译类的静态方法 compile

    public function compile()
    {
    // 首先判断有没有编译过路由
    if (null !== $this->compiled) {
    return $this->compiled;
    }

    // 没有则先获取 `Symfony\Component\Routing\RouteCompiler` Symfony 路由编译类的类名
    $class = $this->getOption('compiler_class');

    // 调用其 compile 方法
    return $this->compiled = $class::compile($this);
    }
  • 第十三步,在 Symfony 路由编译类的 compile 方法中,执行路由编译,主要对路由的 uri 、uri 中的参数、http 请求 token、uri 待匹配的正则表达式进行相关转换和赋值,最后返回编译好的(实例化) Symfony\Component\Routing\CompiledRoute 类对象

    public static function compile(Route $route)
    {
    $hostVariables = array();
    $variables = array();
    $hostRegex = null;
    $hostTokens = array();

    if ('' !== $host = $route->getHost()) {
    $result = self::compilePattern($route, $host, true);

    $hostVariables = $result['variables'];
    $variables = $hostVariables;

    $hostTokens = $result['tokens'];
    $hostRegex = $result['regex'];
    }

    $path = $route->getPath();

    $result = self::compilePattern($route, $path, false);

    $staticPrefix = $result['staticPrefix'];

    $pathVariables = $result['variables'];

    foreach ($pathVariables as $pathParam) {
    if ('_fragment' === $pathParam) {
    throw new \InvalidArgumentException(sprintf('Route pattern "%s" cannot contain "_fragment" as a path parameter.', $route->getPath()));
    }
    }

    $variables = array_merge($variables, $pathVariables);

    $tokens = $result['tokens'];
    $regex = $result['regex'];

    // 最后返回编译好的(实例化) `Symfony\Component\Routing\CompiledRoute` 类对象
    return new CompiledRoute(
    $staticPrefix,
    $regex,
    $tokens,
    $pathVariables,
    $hostRegex,
    $hostTokens,
    $hostVariables,
    array_unique($variables)
    );
    }
  • 第十四步,将编译完的 Symfony\Component\Routing\CompiledRoute 类对象赋值到 Illuminate\Routing\Route 类的 compiled 属性上,然后我们从第八步继续往下说

    protected function compileRoute()
    {
    if (! $this->compiled) {
    // 将编译完的 `Symfony\Component\Routing\CompiledRoute` 类对象赋值到 `Illuminate\Routing\Route` 类的 compiled 属性上
    $this->compiled = (new RouteCompiler($this))->compile();
    }

    return $this->compiled;
    }
    public function matches(Request $request, $includingMethod = true)
    {
    $this->compileRoute();

    // 我们从第八步继续往下说
    foreach ($this->getValidators() as $validator) {
    if (! $includingMethod && $validator instanceof MethodValidator) {
    continue;
    }

    if (! $validator->matches($this, $request)) {
    return false;
    }
    }

    return true;
    }
  • 第十五步,分别实例化 UriValidator 路由 URI 匹配类、MethodValidator HTTP 匹配类、SchemeValidator HTTP 协议验证类、HostValidator 主机域名验证类这四个类

    public static function getValidators()
    {
    if (isset(static::$validators)) {
    return static::$validators;
    }

    // 分别实例化 UriValidator 路由 URI 匹配类、MethodValidator HTTP 匹配类、SchemeValidator HTTP 协议验证类、HostValidator 主机域名验证类这四个类
    return static::$validators = [
    new UriValidator, new MethodValidator,
    new SchemeValidator, new HostValidator,
    ];
    }
  • 第十六步,分别调用上一步实例化好四个类的 matches 方法,如果全都成功返回 true, 那么第七步的 first 方法成功匹配到了第一个路由,后面的将不进行匹配了

    public function matches(Request $request, $includingMethod = true)
    {
    $this->compileRoute();

    foreach ($this->getValidators() as $validator) {
    if (! $includingMethod && $validator instanceof MethodValidator) {
    continue;
    }

    // 分别调用上一步实例化好四个类的 matches 方法
    if (! $validator->matches($this, $request)) {
    return false;
    }
    }

    return true;
    }

    UriValidator

    public function matches(Route $route, Request $request)
    {
    $path = $request->path() == '/' ? '/' : '/'.$request->path();

    // Request URI 与 路由 URI 做正则匹配,成功返回 true
    return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
    }

    MethodValidator

    public function matches(Route $route, Request $request)
    {
    // 请求的 HTTP 方法是否在路由的 methods 中
    return in_array($request->getMethod(), $route->methods());
    }

    SchemeValidator

    public function matches(Route $route, Request $request)
    {
    // HTTP 验证
    if ($route->httpOnly()) {
    return ! $request->secure();
    // HTTPS 验证
    } elseif ($route->secure()) {
    return $request->secure();
    }

    return true;
    }

    HostValidator

    public function matches(Route $route, Request $request)
    {
    if (is_null($route->getCompiled()->getHostRegex())) {
    return true;
    }

    // HOST 正则匹配
    return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());
    }
  • 第十七步,我们回到第二步,现在路由找到了,首先赋值到 Router 类的 current 属性,然后以 Route::class 类名为键,找到的路由对象为值,绑定到 Laravel 容器的 instances 属性上,方便后面执行控制器方法是调用

    protected function findRoute($request)
    {
    // 现在路由找到了,首先赋值到 Router 类的 current 属性
    $this->current = $route = $this->routes->match($request);

    // 以 `Route::class` 类名为键,找到的路由对象为值,绑定到 Laravel 容器的 instances 属性上,方便后面执行控制器方法时调用
    $this->container->instance(Route::class, $route);

    return $route;
    }
  • 第十八步,至此,路由匹配完成