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\RouteCompiler
中compile
方法如下:
//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\RouteCompiler
的compile
返回最終的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路由匹配過程呼叫流程圖