PHP路由本质是将$_SERVER['REQUEST_URI']按规则映射到函数或方法,需Web服务器重写所有非静态请求至index.php,并通过解析路径、正则匹配提取控制器/动作/参数,路由应专注分发,权限校验等交由中间件处理。
PHP 路由不是框架“自带的魔法”,而是你主动设计的一套请求分发逻辑——它本质是把 $_SERVER['REQUEST_URI'] 这个字符串,按规则映射到某个函数或控制器方法上执行。
Web 服务器(Apache/Nginx)必须把非静态资源的请求全部转给 index.php,否则路由代码根本没机会运行。
.htaccess 开启重写RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]try_files 指令兜底location / {
try_files $uri $uri/ /index.php?$query_string;
}⚠️ 容易踩的坑:
mod_rewrite,导致 404 或直接暴露 PHP 文件$query_string,GET 参数丢失!-f 和 !-d 判断原始路径如 /user/profile/123?tab=posts,得先标准化再拆解:
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
/index.php)避免干扰/ 分割,跳过空段(开头斜杠会导致首项为空)常见做法是约定前两段为 控制器/动作,其余为参数:
$parts =array_filter(explode('/', $path)); $controller = $parts[0] ?? 'Home'; $action = $parts[1] ?? 'index'; $params = array_slice($parts, 2);
⚠️ 注意点:
$_GET 拆路径,那是查询参数,不是路由参数array_filter() 必须加,否则 ['', 'user', 'profile'] 会错位/api/v1/users,硬拆成三段就崩了 → 动态路由需正则匹配,不能只靠 explode
静态数组(['/user' => 'UserController@index'])只适合 demo。真实项目要支持:
/user/123 → 提取 id=123
/post/hello-world → 提取 slug=hello-world
/api/v2/users → 匹配前缀并透传版本号所以得用正则逐条匹配:
$routes = [ '#^/user/(\d+)$#' => ['UserController', 'show'], '#^/post/([a-z0-9\-]+)$#' => ['PostController', 'view'], ];foreach ($routes as $pattern => [$ctrl, $act]) { if (preg_match($pattern, $path, $matches)) { array_shift($matches); // 去掉完整匹配项 $params = $matches; require "controllers/{$ctrl}.php"; (new $ctrl())->$act(...$params); exit; } }
⚠️ 关键细节:
# 或 ~ 做分隔符,避免和 URL 中的 / 冲突preg_match 返回的是匹配结果数组,$matches[0] 是全量匹配,真正参数从 $matches[1] 开始http_response_code(404),否则默认返回 200 + 空白页简单项目用 foreach + 正则完全够用;但当路由规则超 50 条、QPS 上千时,线性遍历确实成瓶颈。
/api/v1/、/api/v2/ 共享节点// 缓存一次解析结果,避免重复 preg_match
$cacheKey = md5($path);
if ($cached = apcu_fetch($cacheKey)) {
[$ctrl, $act, $params] = $cached;
} else {
// 执行匹配逻辑...
apcu_store($cacheKey, [$ctrl, $act, $params], 60);
}真正卡顿往往不在匹配本身,而在:
路由最常被忽略的,是它不该承担权限校验、日志记录、输入过滤——这些是中间件的事。把它当成纯粹的“URL→函数”调度器,结构才清晰、才好测、才不会越写越重。