Laravel有很多路由配置,可以设置域名,设置请求协议,设置请求方式,请求路径。那么,Laravel拿到请求后,做了什么来匹配路由呢?本文使用Laravel5.8的源码进行讲解,带你一步步看源码。Laravel的默认路由有四个验证器。UriValidator、MethodValidator、SchemeValidator、HostValidator分别处理uri匹配、请求方法匹配、协议匹配、域名匹配。举几个例子:HostValidator验证域名是否符合域配置Route::domain('{account}.blog.dev')->function({return'Hello';});UriValidator验证请求的uri是否符合路由配置,MethodValidator验证当前请求方法是否为get方法Route::get('/home/posts/{id?}',function($id=null){return'getpost'.$id;})SchemeValidator验证访问协议,主要用于验证安全路由。只能验证http,或者httpsRoute::get('foo',array('https',function(){}));只有当四个验证器都通过时,才认为当前请求匹配成功。这四个验证者是如何验证的?请求方法验证类MethodValidatorimplementsValidatorInterface{publicfunctionmatches(Route$route,Request$request){returnin_array($request->getMethod(),$route->methods());}SchemeValidator}请求方法的验证最简单,就是验证当前请求方法是否为当前路由允许的请求方法。路由允许的请求方法是在实例化路由时创建的。请求协议验证类SchemeValidator实现ValidatorInterface{publicfunctionmatches(Route$route,Request$request){if($route->httpOnly()){return!$请求->安全();}elseif($route->secure()){return$request->secure();}返回真;}}通过获取当前请求的Request,判断是否为https,并与当前路由的配置进行对比域名验证和uri验证本质上都是一样的。通过编译分解路由的配置,获取uri得到匹配域名的正则表达式,然后使用正则表达式进行匹配。如果匹配成功,则验证通过。这里以UriValidator为例classUriValidatorimplementsValidatorInterface{/***根据路由和请求验证给定的规则。**@param\Illuminate\Routing\Route$route*@param\Illuminate\Http\Request$request*@returnbool*/公共函数matches(Route$route,Request$request){$path=$request->path()==='/'?'/':'/'.$request->path();返回preg_match($route->getCompiled()->getRegex(),rawurldecode($path));}}这里的关键是getCompiled返回的对象。getCompiled返回的是Symfony\Component\Routing\CompiledRoute。该对象包含当前路由编译后的uri匹配正则表达式、域名匹配正则表达式等信息。谁返回CompiledRoute?在验证器验证每个路由之前,将执行compileRoute方法以创建一个CompiledRoute对象。//Illuminate\Routing\Routepublicfunctionmatches(Request$request,$includingMethod=true){$this->compileRoute();foreach($this->getValidators()as$validator){if(!$includingMethod&&$validatorinstanceofMethodValidator){继续;}if(!$validator->matches($this,$request)){返回false;}}returntrue;}protectedfunctioncompileRoute(){if(!$this->compiled){$this->compiled=(newRouteCompiler($this))->compile();}return$this->compiled;}Illuminate\Routing\RouteCompiler中编译方法如下://useSymfony\Component\Routing\RouteasSymfonyRoute;publicfunctioncompile(){$optionals=$this->getOptionalParameters();$uri=preg_replace('/\{(\w+?)\?\}/','{$1}',$this->route->uri());返回(newSymfonyRoute($uri,$optionals,$this->route->wheres,['utf8'=>true],$this->route->getDomain()?:''))->compile();}//Symfony\Component\Routing\Route代码//compiler_classSymfony\\Component\\Routing\\RouteCompilerpublicfunctioncompile(){if(null!==$this->compiled){return$this->compiled;}$class=$this->getOption('compiler_class');return$this->compiled=$class::compile($this);}可以看出,final是Symfony\Component\Routing\RouteCompiler的compile最终的compileRoute对象路由编译返回的是什么?//Symfony\Component\Routing\RouteCompiler源码publicstaticfunctioncompile(Route$route){...if(''!==$host=$route->getHost()){$result=self::compilePattern($路线,$主机,真);$hostVariables=$result['变量'];$变量=$主机变量;$hostTokens=$result['tokens'];$hostRegex=$result['regex'];}...}RouteCompiler::compile的入参为当前需要匹配的路由。首先判断路由是否有域名配置。如果有域名配置,编译域名配置的正则表达式,得到匹配的域名正则表达式,表达式中的变量信息已经匹配。//Symfony\Component\Routing\RouteCompiler源码publicstaticfunctioncompile(Route$route){...$path=$route->getPath();$result=self::compilePattern($route,$path,false);$staticPrefix=$result['staticPrefix'];$pathVariables=$result['变量'];...$variables=array_merge($variables,$pathVariables);$tokens=$result['tokens'];$regex=$result['regex'];...}然后获取路由的uri配置,解析配置获取配置中匹配的正则表达式、变量数组、前缀信息。解析域名和路径匹配规则后,根据解析数据创建CompiledRoute对象并返回。因此在路由编译过程中,主要根据路由配置解析出匹配的正则表达式、变量数组、前缀信息。并将解析后的数据创建的CompiledRoute对象返回给调用者。这样调用者就可以直接通过CompiledRoute的属性获取路由解析后的匹配规则。如何解析匹配规则?//Symfony\Component\Routing\RouteCompiler源码privatestaticfunctioncompilePattern(Route$route,$pattern,$isHost){...preg_match_all('#\{(!)?(\w+)\}#',$模式,$匹配,PREG_OFFSET_CAPTURE|PREG_SET_ORDER);foreach($matchesas$match){...if($isSeparator&&$precedingText!==$precedingChar){$tokens[]=['text',substr($precedingText,0,-\strlen($precedingChar))];}elseif(!$isSeparator&&\strlen($precedingText)>0){$tokens[]=['text',$precedingText];}...如果($important){$token=['variable',$isSeparator?$precedingChar:'',$regexp,$varName,false,true];}else{$token=['变量',$isSeparator?$precedingChar:'',$regexp,$varName];}...}...}首先通过正则表达式匹配是否为变量配置,例如Route::get('/posts/{id}'),Route::domain('{account}.blog.dev')。如果有变量,则截取配置规则,$tokens[]=['text',$precedingText];对于不包含变量的配置规则部分,$token=['variable',$isSeparator?$forallvariablesprecedingChar:'',$regexp,$varName,false,true]保存解析后的信息。//Symfony\Component\Routing\RouteCompilersourceprivatestaticfunctioncompilePattern(Route$route,$pattern,$isHost){...if($pos<\strlen($pattern)){$tokens[]=['text',substr($pattern,$pos)];}//找到第一个可选标记$firstOptional=PHP_INT_MAX;如果(!$isHost){对于($i=\count($tokens)-1;$i>=0;--$i){$token=$tokens[$i];//当变量不重要时它是可选的并且有一个默认值if('variable'===$token[0]&&!($token[5]??false)&&$route->hasDefault($token[3])){$firstOptional=$i;}else{休息;}}}...当配置信息不包含任何变量时,则在这段代码中进入第一个if判断,将匹配规则保存在token数组中。区分当前解析是匹配域名还是uri,如果匹配uri,则找出第一个可选参数在变量中的位置。这一步是将路由配置转换为匹配的规则令牌。通过每个token生成匹配的正则表达式很方便。//Symfony\Component\Routing\RouteCompiler源代码privatestaticfunctioncomputeRegexp(array$tokens,int$index,int$firstOptional):string{$token=$tokens[$index];if('text'===$token[0]){returnpreg_quote($token[1],self::REGEX_DELIMITER);}else{if(0===$index&&0===$firstOptional){returnsprintf('%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){//关闭可选子模式$regexp.=str_repeat(')?',$nbTokens-$firstOptional-(0===$firstOptional?1:0));}}返回$正则表达式;}}}解析得到的token数组保存了所有匹配的规则数组。如果当前匹配的规则token是文本类型,则对字符串进行转义,作为匹配的正则表达式返回。如果是变量,则根据是否可选(第一个可选参数的位置在上一步中已经找到)在正则表达式中加上可选标志。//Symfony\Component\Routing\RouteCompilersourceprivatestaticfunctioncompilePattern(Route$route,$pattern,$isHost){...$regexp='';对于($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':'');...返回['staticPrefix'=>self::determineStaticPrefix($route,$tokens),'regex'=>$regexp,'tokens'=>array_reverse($tokens),'variables'=>$variables,];根据每个token获取每个匹配规则的正则表达式,将所有的正则表达式拼接成一个正则表达式公式,前缀和后缀为正则表达式。这样就得到了一个完整的可以匹配的正则表达式。然后将前缀、匹配的正则表达式、匹配的规则数组标记和变量数组返回给调用者。为调用者生成一个CompiledRoute对象。附上Laravel路由匹配过程调用流程图
