老王带你看源码|Laravel 的路由匹配的过程都干了些什么?

Laravel 的路由配置有很多,可以设置域名,设置请求协议,设置请求方式,请求路径。那么,Laravel在获取到请求之后,去匹配路由都做了些什么呢?本文以Laravel5.8源码讲解,带你一步步看源码。

文章首发于公众号【写PHP的老王】,更多内容可以关注公众号

Laravel 路由解析实例

Laravel 默认路由的验证器有四个,UriValidator,MethodValidator,SchemeValidator,HostValidator分别处理uri的匹配,请求方法的匹配,协议的匹配,域名的匹配。

举几个例子:

  • HostValidator验证域名是符合domain的配置
Route::domain('{account}.blog.dev')->function({
    return 'Hello';
});
  • UriValidator验证请求的uri是否符合路由配置,MethodValidator验证当前请求方法是否是get方法

    Route::get('/home/posts/{id?}',function($id=null){
    return 'get post '.$id;
    })
  • SchemeValidator验证访问协议,主要用于验证安全路由。只能验证是http,或者https

Route::get('foo', array('https', function(){}));

只有当四个验证器都通过才认为当前请求匹配路由成功。

那这四个验证器都是怎么验证的呢?

Laravel 路由匹配验证器

请求方法验证

class MethodValidator implements ValidatorInterface
{
    public function matches(Route $route, Request $request)
    {
        return in_array($request->getMethod(), $route->methods());
    }
    SchemeValidator
}

请求方式的验证最简单,就是验证当前请求方式是否是当前路由允许的请求方式。而路由的允许的请求方式在路由实例化的时候就创建好了。

请求协议验证

class SchemeValidator implements ValidatorInterface
{
    public function matches(Route $route, Request $request)
    {
        if ($route->httpOnly()) {
            return ! $request->secure();
        } elseif ($route->secure()) {
            return $request->secure();
        }

        return true;
    }
}

通过获取当前请求的Request,判断是否是https,与当前路由的配置进行比较

域名验证以及uri的验证

这两种验证本质上都是一样的。通过对路由的配置进行编译分解,获取uri获取域名匹配的正则表达式,然后通过正则表达式进行匹配。如果匹配成功,则验证通过。

这里以UriValidator为例说明

class UriValidator implements ValidatorInterface
{
    /**
     * Validate a given rule against a route and request.
     *
     * @param  \Illuminate\Routing\Route  $route
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    public function matches(Route $route, Request $request)
    {
        $path = $request->path() === '/' ? '/' : '/'.$request->path();

        return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
    }
}

这里的关键是getCompiled返回的这个对象。getCompiled返回的是Symfony\Component\Routing\CompiledRoute这个对象包含了当前路由编译之后的uri匹配正则表达式,域名匹配正则表达式等信息。

CompiledRoute是谁返回的?

在每个路由获取验证器进行验证之前,都会执行compileRoute方法创建CompiledRoute对象。

//Illuminate\Routing\Route
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;
}
protected function compileRoute()
{
    if (! $this->compiled) {
        $this->compiled = (new RouteCompiler($this))->compile();
    }
    return $this->compiled;
}

Illuminate\Routing\RouteCompilercompile方法如下:

//use Symfony\Component\Routing\Route as SymfonyRoute;
public function compile()
{
    $optionals = $this->getOptionalParameters();
    $uri = preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri());
    return (
        new SymfonyRoute($uri, $optionals, $this->route->wheres, ['utf8' => true], $this->route->getDomain() ?: '')
    )->compile();
}
//Symfony\Component\Routing\Route 代码
//compiler_class Symfony\\Component\\Routing\\RouteCompiler
public function compile()
{
    if (null !== $this->compiled) {
        return $this->compiled;
    }
    $class = $this->getOption('compiler_class');
    return $this->compiled = $class::compile($this);
}

可以看出,最终是由Symfony\Component\Routing\RouteCompilercompile返回最终的compileRoute对象。

路由编译都干了些什么?

//Symfony\Component\Routing\RouteCompiler 源码
public static function compile(Route $route)
{
    ...
    if ('' !== $host = $route->getHost()) {
        $result = self::compilePattern($route, $host, true);

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

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

RouteCompiler::compile输入参数是当前需要匹配的路由。首先判断路由是否有域名配置,如果有域名配置则对域名配置进行正则表达式编译,获取域名的匹配正则表达式,已经匹配表达式中的变量信息。

//Symfony\Component\Routing\RouteCompiler 源码
public static function compile(Route $route)
{
    ...
    $path = $route->getPath();
    $result = self::compilePattern($route, $path, false);
    $staticPrefix = $result['staticPrefix'];
    $pathVariables = $result['variables'];
    ...
    $variables = array_merge($variables, $pathVariables);
    $tokens = $result['tokens'];
    $regex = $result['regex'];
    ...
}

然后获取路由的uri配置,对配置进行解析获取配置中的匹配正则表达式,变量数组,前缀信息。

域名,路径匹配规则解析之后,根据解析后的数据创建一个CompiledRoute对象,并返回

因此,在路由编译过程中,主要是根据路由配置,解析出匹配的正则表达式,变量数组,前缀信息。并将这些解析之后的数据创建的CompiledRoute对象返回给调用方。这样,调用方就能够直接通过CompiledRoute的属性直接获取到路由解析之后的匹配规则。

匹配规则怎么解析的?

//Symfony\Component\Routing\RouteCompiler 源码
private static function compilePattern(Route $route, $pattern, $isHost)
{
    ...
    preg_match_all('#\{(!)?(\w+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
    foreach ($matches as $match) {
        ...
        if ($isSeparator && $precedingText !== $precedingChar) {
            $tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))];
        } elseif (!$isSeparator && \strlen($precedingText) > 0) {
            $tokens[] = ['text', $precedingText];
        }
        ...
        if ($important) {
            $token = ['variable', $isSeparator ? $precedingChar : '', $regexp, $varName, false, true];
        } else {
            $token = ['variable', $isSeparator ? $precedingChar : '', $regexp, $varName];
        }
        ...
    }
    ...
}

首先通过正则表达式匹配是否由变量配置,例如Route::get('/posts/{id}'),Route::domain('{account}.blog.dev')。如果有变量,则对配置规则进行截取,将配置规则中不包含变量的部分$tokens[] = ['text', $precedingText]; ,对所有变量$token = ['variable', $isSeparator ? $precedingChar : '', $regexp, $varName, false, true]保存解析后的信息。

//Symfony\Component\Routing\RouteCompiler 源码
private static function compilePattern(Route $route, $pattern, $isHost)
{
    ...
    if ($pos < \strlen($pattern)) {
        $tokens[] = ['text', substr($pattern, $pos)];
    }
    // find the first optional token
    $firstOptional = PHP_INT_MAX;
    if (!$isHost) {
        for ($i = \count($tokens) - 1; $i >= 0; --$i) {
            $token = $tokens[$i];
            // variable is optional when it is not important and has a default value
            if ('variable' === $token[0] && !($token[5] ?? false) && $route->hasDefault($token[3])) {
                $firstOptional = $i;
            } else {
                break;
            }
        }
    }
    ...

当配置信息中不包含任何变量,则进入这段代码中第一个if判断里面,将匹配规则保存在token数组中。

区分当前解析是对域名的匹配还是对uri的匹配,如果对uri的匹配,则找出变量中第一个可选参数的位置。

这一步是把路由配置转换成可匹配的规则token。方便后续通过每个token生成匹配正则表达式。

//Symfony\Component\Routing\RouteCompiler 源码
private static function computeRegexp(array $tokens, int $index, int $firstOptional): string
{
    $token = $tokens[$index];
    if ('text' === $token[0]) {
        return preg_quote($token[1], self::REGEX_DELIMITER);
    } else {
        if (0 === $index && 0 === $firstOptional) {
            return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
        } else {
            $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
            if ($index >= $firstOptional) {
                $regexp = "(?:$regexp";
                $nbTokens = \count($tokens);
                if ($nbTokens - 1 == $index) {
                    // Close the optional subpatterns
                    $regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
                }
            }
            return $regexp;
        }
    }
}

通过解析获取的token数组,保存了所有的匹配规则数组。如果当前匹配规则token是text类型,则在对字符串进行转义处理,返回作为匹配的正则表达式。

如果是变量,则根据是否是可选的(上一步已经找到了第一个可选参数的位置),在正则表达式中添加可选标识。

//Symfony\Component\Routing\RouteCompiler 源码
private static function compilePattern(Route $route, $pattern, $isHost)
{
    ...
    $regexp = '';
    for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) {
        $regexp .= self::computeRegexp($tokens, $i, $firstOptional);
    }
    $regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'sD'.($isHost ? 'i' : '');
    ...
    return [
        'staticPrefix' => self::determineStaticPrefix($route, $tokens),
        'regex' => $regexp,
        'tokens' => array_reverse($tokens),
        'variables' => $variables,
    ];

根据每个token获取每个匹配规则的正则表达式,将所有的正则表达式拼接成一个正则表达式,并加上正则表达式前后缀。这样就获取了一个完整可匹配的正则表达式。

然后将前缀,匹配正则表达式,匹配规则数组tokens,变量数组返回给调用方。供调用方生成CompiledRoute对象。

总结

文章比较长,主要是根据调用链一步步分析每个方法的作用,介绍Laravel如何实现路由匹配的动能,希望对大家有帮助。最后附上Laravel路由匹配过程调用流程图

Laravel 的路由匹配都干了些什么

本作品采用《CC 协议》,转载必须注明作者和本文链接
写PHP的老王
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!