路由调度之路由匹配
简介
要实现浏览器 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;
}第十八步,至此,路由匹配完成