前言

上一篇文章我们说到路由的正则编译,正则编译的目的就是和请求的 url 来匹配,只有匹配上的路由才是我们真正想要的,此外也会通过正则匹配来获取路由的参数。

路由的匹配

路由进行正则编译后,就要与请求 request 来进行正则匹配,并且进行一些验证,例如 UriValidatorMethodValidatorSchemeValidatorHostValidator

class RouteCollection implements Countable, IteratorAggregate
{
	public function match(Request $request)
    {
        $routes = $this->get($request->getMethod());

        $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;
    }
    
    protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
	{
   		 return Arr::first($routes, function ($value) use ($request, $includingMethod) {
	    	 return $value->matches($request, $includingMethod);
   		 });
	}
}
    
class 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;
	}

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

    	return static::$validators = [
        	new UriValidator, new MethodValidator,
        	new SchemeValidator, new HostValidator,
    	];
	}
}

UriValidator uri 验证

UriValidator 验证主要是目的是查看路由正则与请求是否匹配:

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

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

值得注意的是,在匹配路径之前,程序使用了 rawurldecode 来对请求进行解码。

MethodValidator 验证

请求方法验证:

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

SchemeValidator 验证

路由 scheme 协议验证:

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;
    }
}

public function httpOnly()
{
    return in_array('http', $this->action, true);
}

public function secure()
{
    return in_array('https', $this->action, true);
}

HostValidator 验证

主域验证:

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

        return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());
    }
}

也就是说,如果路由中并不设置 host 属性,那么这个验证并不进行。

路由的参数绑定

一旦某个路由符合请求的 uri 四项认证,就将会被返回,接下来就要对路由的参数进行绑定与赋值:

class RouteCollection implements Countable, IteratorAggregate
{
    public function bind(Request $request)
    {
        $this->compileRoute();

        $this->parameters = (new RouteParameterBinder($this))
                        ->parameters($request);

        return $this;
    }
}

bind 函数负责路由参数与请求 url 的绑定工作:

class RouteParameterBinder
{
	public function parameters($request)
    {
        $parameters = $this->bindPathParameters($request);

        if (! is_null($this->route->compiled->getHostRegex())) {
            $parameters = $this->bindHostParameters(
                $request, $parameters
            );
        }

        return $this->replaceDefaults($parameters);
    }
}

可以看出,路由参数绑定分为主域参数绑定与路径参数绑定,我们先看路径参数绑定:

路径参数绑定

class RouteParameterBinder
{
	protected function bindPathParameters($request)
	{
  		  preg_match($this->route->compiled->getRegex(), '/'.$request->decodedPath(), $matches);

  	 	 return $this->matchToKeys(array_slice($matches, 1));
	}
}

例如,{foo}/{baz?}.{ext?} 进行正则编译后结果:

#^/(?P<foo>[^/]++)(?:/(?P<baz>[^/\.]++)(?:\.(?P<ext>[^/]++))?)?$#s

其与 request 匹配后的结果为:

$matches = array (
    0   = "/foo/baz.ext",
    1   = "foo",
    foo = "foo",
    2   = "baz",
    baz = "baz",
    3   = "ext",
    ext = "ext",
)

array_slice($matches, 1) 取出了 $matches 数组 1 之后的结果,然后调用了 matchToKeys 函数,

protected function matchToKeys(array $matches)
{
	 if (empty($parameterNames = $this->route->parameterNames())) {
  		  return [];
	 }

	$parameters = array_intersect_key($matches, array_flip($parameterNames));

	return array_filter($parameters, function ($value) {
   		 return is_string($value) && strlen($value) > 0;
	 });
}

该函数中利用正则获取了路由的所有参数:

class Route
{
	public function parameterNames()
    {
        if (isset($this->parameterNames)) {
            return $this->parameterNames;
        }

        return $this->parameterNames = $this->compileParameterNames();
    }

    
    protected function compileParameterNames()
    {
        preg_match_all('/\{(.*?)\}/', $this->domain().$this->uri, $matches);

        return array_map(function ($m) {
            return trim($m, '?');
        }, $matches[1]);
    }
}

可以看出,获取路由参数的正则表达式采用了勉强模式,意图提取出所有的路由参数。否则,对于路由 {foo}/{baz?}.{ext?},贪婪型正则表达式 /\{(.*)\}/ 将会匹配整个字符串,而不是各个参数分组。

提取出的参数结果为:

$matches = array (
	0 = array (
		0 = "{foo}".
		1 = "{baz?}",
		2 = "{ext?}",
	)
	1 = array (
		0 = "foo".
		1 = "baz?",
		2 = "ext?",
	)
)

得出的结果将会去除 $matches[1],并且将会删除结果中最后的 ?

之后,在 matchToKeys 函数中,

$parameters = array_intersect_key($matches, array_flip($parameterNames));

获取了匹配结果与路由所有参数的交集:

$parameters = array (
   foo = "foo",
   baz = "baz",
   ext = "ext",
)

主域参数绑定

protected function bindHostParameters($request, $parameters)
{
    preg_match($this->route->compiled->getHostRegex(), $request->getHost(), $matches);

    return array_merge($this->matchToKeys(array_slice($matches, 1)), $parameters);
}

步骤与路由参数绑定一致。

替换默认值

进行参数绑定后,有一些可选参数并没有在 request 中匹配到,这时候就要用可选参数的默认值添加到变量 parameters 中:

protected function replaceDefaults(array $parameters)
{
    foreach ($parameters as $key => $value) {
        $parameters[$key] = isset($value) ? $value : Arr::get($this->route->defaults, $key);
    }

    foreach ($this->route->defaults as $key => $value) {
        if (! isset($parameters[$key])) {
            $parameters[$key] = $value;
        }
    }

    return $parameters;
}

匹配异常处理

如果 url 匹配失败,没有找到任何路由与请求相互匹配,就会切换 method 方法,以求任意路由来匹配:

protected function checkForAlternateVerbs($request)
{
    $methods = array_diff(Router::$verbs, [$request->getMethod()]);

    $others = [];

    foreach ($methods as $method) {
        if (! is_null($this->matchAgainstRoutes($this->get($method), $request, false))) {
            $others[] = $method;
        }
    }

    return $others;
}

如果使用其他方法匹配成功,就要判断当前方法是否是 options,如果是则直接返回,否则报出异常:

protected function getRouteForMethods($request, array $methods)
{
    if ($request->method() == 'OPTIONS') {
        return (new Route('OPTIONS', $request->path(), function () use ($methods) {
            return new Response('', 200, ['Allow' => implode(',', $methods)]);
        }))->bind($request);
    }

    $this->methodNotAllowed($methods);
}

protected function methodNotAllowed(array $others)
{
    throw new MethodNotAllowedHttpException($others);
}

书籍推荐