路由调度之路由匹配
简介
要实现浏览器 URL 定位到 Laravel 具体控制器方法,首先要做的工作就是路由匹配。
路由匹配,顾名思义:就是我们浏览器输入的 URL 与我们 Laravel 定义的路由能够一一对应,从而执行相应控制器方法,来实现我们的业务。
浏览器:
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 路由编译类的静态方法 compilepublic 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; }
-
第十八步,至此,路由匹配完成
本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。