thinkphp5.1原始碼閱讀與學習(一、路由解析)

K_G發表於2021-02-20

1 路由解析

在App的run()方法初始化後,開始進行請求路由解析

請求解析的過程就是講url根據註冊的路由分派到對應的應用層業務邏輯

請求解析的實現由/library/think/Route實現

2 路由解析入口原始碼分析

//App run()方法

// 初始化應用
$this->initialize();

if ($this->bind) {
    // 模組/控制器繫結
    $this->route->bind($this->bind);
} elseif ($this->config('app.auto_bind_module')) {
    // 入口自動繫結
    $name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME);
    if ($name && 'index' != $name && is_dir($this->appPath . $name)) {
        $this->route->bind($name);
    }
}

首先檢查App的屬性bind,是否繫結到特定模組/控制器
如果App的屬性bind沒有設定,則讀取配置的app.auto_bind_module
如果設定了自動繫結模組,則將入口檔名繫結為模組名稱。
比如設定app的auto_bind_module為ture。則訪問admin.php入口檔案。
那麼預設的模組就是admin模組

// 監聽app_dispatch
$this->hook->listen('app_dispatch');

呼叫app_dispatch的回撥函式

$dispatch = $this->dispatch;
if (empty($dispatch)) {
    // 進行URL路由檢測
    $this->route->lazy($this->config('app.url_lazy_route'));
    $dispatch = $this->routeCheck();
}
$this->request->dispatch($dispatch);

獲取應用的排程資訊。這裡的routeCheck()是路由解析的入口
然後將解析的排程資訊儲存到全域性Request物件中。

if ($this->debug) {
                $this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true));
                $this->log('[ HEADER ] ' . var_export($this->request->header(), true));
                $this->log('[ PARAM ] ' . var_export($this->request->param(), true));
            }

除錯模式下,儲存路由的請求資訊到日誌檔案中

 // 監聽app_begin
$this->hook->listen('app_begin');

呼叫app_begin的回撥函式

$this->request->cache(
    $this->config('app.request_cache'),
    $this->config('app.request_cache_expire'),
    $this->config('app.request_cache_except')
);

檢查否開啟了請求快取,
如果設定了快取,則嘗試讀取快取結果

// 執行排程
$data = $dispatch->run();

這裡執行排程。也就是執行請求分派到的業務邏輯,
通常是模組/控制器中的特定方法。也可以是其他形式的業務邏輯
比如 閉包函式,或者直接返回模板等。

 $this->middlewareDispatcher->add(function (Request $request, $next) use ($data) {
            // 輸出資料到客戶端
            if ($data instanceof Response) {
                $response = $data;
            } elseif (!is_null($data)) {
                // 預設自動識別響應輸出型別
                $isAjax = $request->isAjax();
                $type   = $isAjax ? $this->config('app.default_ajax_return') : $this->config('app.default_return_type');

                $response = Response::create($data, $type);
            } else {
                $response = Response::create();
            }
            return $response;
        });

        $response = $this->middlewareDispatcher->dispatch($this->request);

這裡是think5.1準備新增的中介軟體功能。

 $this->hook->listen('app_end', $response);
 return $response;

排程執行完後,呼叫app_end的回撥函式
最後run()方法返回建立的響應物件Response
然後在入口檔案index.php中呼叫Response的send()將結果輸出到客戶端
Container::get('app')->run()->send();

3 路由註冊過程原始碼分析

由上面的分析可知 路由解析的入口是App的routeCheck()
下面開始分析App的routeCheck()是如何解析請求Request得到排程資訊的

//App routeCheck()

$path = $this->request->path();
$depr = $this->config('app.pathinfo_depr');

呼叫Request的path()方法獲取當前請求的pathinfo資訊
然後讀取app.pathinfo_depr獲取pathinfo的分隔符
這裡的path就是請求的urlindex/blog/index。pathifo_depr也就是url分隔符/

$files = scandir($this->routePath);
foreach ($files as $file) {
    if (strpos($file, '.php')) {
        $filename = $this->routePath . $file;
        // 匯入路由配置
        $rules = include $filename;
        if (is_array($rules)) {
            $this->route->import($rules);
        }
    }
}

這裡遍歷路由目錄/route/中的所有檔案。將其中的路由規則匯入
也就是將配置的路由資訊載入到框架中。
然後根據註冊的路由資訊匹配請求url

if ($this->config('app.route_annotation')) {
        // 自動生成路由定義
        if ($this->debug) {
            $this->build->buildRoute($this->config('app.controller_suffix'));
        }

        $filename = $this->runtimePath . 'build_route.php';

        if (is_file($filename)) {
            include $filename;
        }
    }

這裡檢查是否配置了註釋自動生成路由
如果開啟了註釋路由,則呼叫Build的buildRoute()解析註釋為路由
然後將解析後的路由檔案build_route.php載入到框架中

 $must = !is_null($this->routeMust) ? $this->routeMust : $this->config('app.url_route_must');

  return $this->route->check($path, $depr, $must, $this->config('app.route_complete_match'));

檢查配置url_route_must是否開啟。
如果開啟,則每個請求必須配置路由,否則預設按照模組/控制器/操作解析
最後呼叫Route的check()方法在路由中匹配url。返回匹配後的排程資訊Dispatch

4 url與路由匹配過程原始碼分析

在Route的check()開始在註冊的路由中匹配url

$domain = $this->checkDomain();

首先檢查是否註冊了請求的域名路由資訊。
註冊的域名路由資訊儲存在Route的domains屬性中
如果註冊了域名路由資訊,則返回對應的域名路由資訊

$url    = str_replace($depr, '|', $url);

將url的分隔符替換為|

 $result = $domain->check($this->request, $url, $depr, $completeMatch);

呼叫域名路由的Check()方法,匹配請求url

這裡的check()方法在/route/Domain.php檔案中
主要進行路由的別名和url繫結檢查 checkouteAlias() checkUrlBind()
路由的別名和url繫結檢查由Route的getAlias()和getBind()實現
在getAlias()和getBind()中主要讀取了Route的alias和bind屬性
檢查是否包含了當前url和域名對應的路由資訊

最後呼叫Domain的父物件組路由RuleGroup的check()進行路由檢查
在RuleGroup()首先檢查跨域請求checkCrossDomain()
然後檢查路由規則的option引數是否有效checkOption()
然後檢查分組路由的url是否匹配checkUrl()

 if ($this->rule) {
    if ($this->rule instanceof Response) {
        return new ResponseDispatch($this->rule);
    }

    $this->parseGroupRule($this->rule);
}

如果上述條件都符合,那麼當前的路由規則就是請求url對應的路由
然後讀取路由中註冊的排程資訊rule
如果註冊的路由排程資訊rule是排程物件,則直接返回撥度物件
否則呼叫分組路由解析。也就是生成分組的多條路由排程資訊rule

 // 分組匹配後執行的行為
$this->afterMatchGroup($request);

// 獲取當前路由規則
$method = strtolower($request->method());
$rules  = $this->getMethodRules($method);

if ($this->parent) {
    // 合併分組引數
    $this->mergeGroupOptions();
}

if (isset($this->option['complete_match'])) {
    $completeMatch = $this->option['complete_match'];
}

if (!empty($this->option['merge_rule_regex'])) {
    // 合併路由正則規則進行路由匹配檢查
    $result = $this->checkMergeRuleRegex($request, $rules, $url, $depr, $completeMatch);

    if (false !== $result) {
        return $result;
    }
}
// 檢查分組路由
foreach ($rules as $key => $item) {
    $result = $item->check($request, $url, $depr, $completeMatch);

    if (false !== $result) {
        return $result;
    }
}

接下來在生成的分組路由的多條排程資訊中匹配請求的url
得到匹配的結果$result。

 if ($this->auto) {
    // 自動解析URL地址
    $result = new UrlDispatch($this->auto . '/' . $url, ['depr' => $depr, 'auto_search' => false]);
} elseif ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) {
    // 未匹配所有路由的路由規則處理
    $result = $this->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->getOption());
} else {
    $result = false;
}

return $result;

如果在分組路由中沒有找到url對應的路由規則
則在auto和miss分組路由中嘗試匹配。
最後返回匹配的結果
也就是生成的排程資訊

if (false === $result && !empty($this->cross)) {
            // 檢測跨域路由
            $result = $this->cross->check($this->request, $url, $depr, $completeMatch);
 }

這裡返回到Route的路由檢查check()方法中
如果沒有匹配到當前url。則檢查是否設定跨域路由
嘗試檢查跨域路由

if (false !== $result) {
    // 路由匹配
    return $result;
} elseif ($must) {
    // 強制路由不匹配則丟擲異常
    throw new RouteNotFoundException();
}

如果得到匹配的路由排程資訊則返回$result
否則檢查是否設定強制路由,
開啟強制路由時,匹配路由失敗則跑出路由不匹配異常

return new UrlDispatch($url, ['depr' => $depr, 'auto_search' => $this->config->get('app.controller_auto_search')]);

如果沒有開啟強制路由,則返回url到模組/控制器/操作的url排程物件
得到排程物件,返回App的run()中開始記錄排程資訊到request
然後呼叫排程物件Dispatch的run()方法

5 排程物件的執行

排程物件Dispatch在think中由/route/dispatch/目錄中實現
主要包括繼承了基礎排程物件dispath的閉包回撥,控制器,模組,跳轉,url,檢視等排程物件
在url排程物件中呼叫了模組排程物件,在模組排程物件中最終執行了業務邏輯控制器的操作。
操作的執行結果返回到App的run()方法中儲存到$data
然後建立對應的響應物件Response
響應物件在/response/中實現為json,jsonp,jump,redirect,view,xml等響應物件
其中的view也就是通常的模板響應物件

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章