Menu

路由调度之路由匹配

简介

要实现浏览器 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;
    }
  • 第十八步,至此,路由匹配完成
本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。

本文章首发在 LearnKu.com 网站上。
上一篇 下一篇
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
发起讨论 只看当前版本


暂无话题~