ThinkPHP 3.x 的路由系統支援 規則路由(規則字串解析) 與 正則路由(PCRE 直接匹配) 兩種模式,並整合 Apache mod_rewrite 實現偽靜態。本文完整還原核心解析函式 parseUrlRule()、parseUrlRuleRegx(),並解析參數提取、多規則合併、分組/模組/操作識別等關鍵機制。
$route = [
// 規則路由:動態參數用 :name,:name^ 排除值,:named 僅匹配數字
'news/:action/:year/:month/:day' => 'news/read?year=:2&month=:3&day=:4',
// 陣列格式:[目標URL, 額外合併參數]
'news/:action^delete|update|insert/:year/:month/:day' => [
'news/read?extra=:2&status=1',
'year=:2&month=:3&day=:4'
],
// 正則路由:鍵為 PCRE 模式
'/news/read/(\d+)/(\d+)/(\d+)/' => 'news/read?year=:1&month=:2&day=:3',
];
<?php
function parseUrlRule(string $rule, $route, string $pathinfo, string $ext = 'html'): bool {
// 1. 移除副檔名
$pathinfo = str_replace('.' . $ext, '', $pathinfo);
// 2. 切割規則與請求路徑
$ruleParts = explode('/', $rule);
$pathParts = explode('/', $pathinfo);
// 3. 逐段匹配
$matches = [];
foreach ($ruleParts as $i => $part) {
if (strpos($part, ':') === 0) { // 動態參數
$key = substr($part, 1);
$isNumeric = substr($key, -1) === 'd'; // :year_d
$exclude = false;
if (strpos($key, '^')) { // :action^delete|update
[$key, $excludeList] = explode('^', $key);
$exclude = explode('|', $excludeList);
}
$val = $pathParts[$i] ?? '';
if ($isNumeric && !ctype_digit($val)) return false;
if ($exclude && in_array($val, $exclude)) return false;
$matches[$key] = $val;
} else { // 靜態段落,不分大小寫比對
if (strcasecmp($part, $pathParts[$i] ?? '') !== 0) return false;
}
}
// 4. 組裝目標 URL
$targetUrl = is_array($route) ? $route[0] : $route;
// 替換 :1, :2... 為對應匹配值
$targetUrl = preg_replace_callback('/:(\d+)/', function($m) use ($matches, $ruleParts) {
// 依規則順序取值
$keys = array_keys($matches);
return $matches[$keys[$m[1]-1]] ?? '';
}, $targetUrl);
// 5. 解析目標 URL 為 module/action + query 參數
parseTargetUrl($targetUrl, $matches, $pathParts, $route);
return true;
}
function parseTargetUrl(string $url, array $matches, array $remainingPath, $route) {
// 解析 module/action
if (strpos($url, '?') !== false) {
[$path, $query] = explode('?', $url, 2);
parse_str($query, $queryParams);
} else {
$path = $url;
$queryParams = [];
}
$pathParts = explode('/', $path);
$_GET['actionName'] = array_pop($pathParts);
$_GET['moduleName'] = array_pop($pathParts);
if ($pathParts) $_GET['groupName'] = array_pop($pathParts);
// 合併路由規則中的參數
$_GET = array_merge($_GET, $queryParams, $matches);
// 合併剩餘路徑參數(如 extraparam/test)
preg_replace_callback('/(\w+)\/([^\/]+)/', function($m) {
$_GET[$m[1]] = $m[2];
}, implode('/', $remainingPath));
// 陣列路由的第二個元素
if (is_array($route) && isset($route[1])) {
parse_str($route[1], $extra);
$_GET = array_merge($_GET, $extra);
}
$_GET['finalUrl'] = $url;
$_REQUEST = array_merge($_REQUEST, $_GET);
}
parseUrlRuleRegx()
function parseUrlRuleRegx(string $regex, $route, string $pathinfo, string $ext = 'html'): bool {
$pathinfo = str_replace('.' . $ext, '', $pathinfo);
$targetUrl = is_array($route) ? $route[0] : $route;
if (!preg_match($regex, $pathinfo, $matches)) return false;
// $matches[0] 為完整匹配,[1] [2]... 為捕獲組
$targetUrl = preg_replace_callback('/:(\d+)/', function($m) use ($matches) {
return $matches[$m[1]] ?? '';
}, $targetUrl);
// 移除已匹配部分,剩餘路徑轉參數
$remaining = str_replace($matches[0], '', $pathinfo);
parseTargetUrl($targetUrl, [], explode('/', trim($remaining, '/')), $route);
return true;
}
# .htaccess 或 httpd.conf
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
# 將 /news/read/2012/2/21 轉為 /index.php/news/read/2012/2/21
# PHP 端 $_SERVER['PATH_INFO'] 即為 news/read/2012/2/21
| 請求 URL | 匹配規則 | 產生 $_GET |
|---|---|---|
| /news/read/2012/2/21/extraparam/test.html | news/:action/:year/:month/:day | ['actionName'=>'read','moduleName'=>'news','year'=>'2012','month'=>'2','day'=>'21','extraparam'=>'test'] |
| /news/delete/2012/2/21.html | news/:action^delete|update/:year/:month/:day | 不匹配(action 被排除) |
| /news/read/2012/2/21.html | /news/read/(\d+)/(\d+)/(\d+)/ | ['actionName'=>'read','moduleName'=>'news','year'=>'2012','month'=>'2','day'=>'21'] |
// TP3 規則路由
'news/:action/:year/:month/:day' => 'news/read?year=:2&month=:3&day=:4'
// TP5/6/8 路由定義(route.php)
Route::rule('news/:action/:year/:month/:day', 'news/read', 'GET', [], [
'year' => '\d{4}',
'month' => '\d{1,2}',
'day' => '\d{1,2}',
]);
// TP5/6/8 正則路由
Route::rule('/news/read/(\d+)/(\d+)/(\d+)/', 'news/read?year=:1&month=:2&day=:3');
ThinkPHP 3.x 的路由解析器雖屬手工打造,但其「規則字串轉正則、動態參數提取、多來源參數合併(路由規則+剩餘路徑+陣列額外參數)」的設計,完整支援了 MVC 分派所需的所有資訊。理解此機制,不僅利於維護舊專案,更能幫助你在設計自有框架或中介軟體時,建立清晰的「請求路徑 → 參數陣列 → 控制器動作」轉換管線。