路由
OxPHP 使用三种模式之一来路由传入的 HTTP 请求,由单个环境变量控制。每种模式都对应一份你熟悉的 nginx try_files 配置,因此你可以准确预测任意 URL 会发生什么。
工作原理
在进入各模式专属逻辑之前,每个请求都会先经过一条共享的处理流水线:
- 点路径过滤 — 包含隐藏段(
.git、.env)的路径会被拦截,但/.well-known/*除外(RFC 8615) - 路由缓存查找 — 最近解析过的 URI 会从一个 LRU 缓存(10 000 条)中直接返回
- 百分号解码 + 净化 — 像
%2e%2e这样的编码序列会被解码,遍历段(..、.、空段)会被剥离 - well-known PHP 拦截 — 纵深防御:
/.well-known/内的.php脚本永不执行 - URI 分类 — 净化后的路径会被一次性分类为
NoExtension、Php或OtherExtension - 模式派发 — 每种模式用各自的规则处理这三类 URI
- 符号链接校验 — 每个解析出的文件系统路径都必须规范化到文档根目录之内
分类这一步是效率的关键:对静态资源(/style.css、/logo.png)的磁盘检查在共享层中对 OtherExtension 类 URI 只做一次,因此三种模式付出的代价相同。
配置
| 变量 | 默认值 | 说明 |
|---|---|---|
DOCUMENT_ROOT |
/var/www/html/public |
用于提供文件和 PHP 脚本的根目录 |
ENTRY_FILE |
(未设置) | 单个规范入口脚本。未设置 = 传统模式。*.php = 框架模式。非 .php = SPA 模式。配合 WORKER_MODE_ENABLED=true = 工作进程模式。接受绝对路径或相对于 DOCUMENT_ROOT 的路径(允许 ..);解析出的路径必须存在 |
WORKER_MODE_ENABLED |
false |
启用持久化工作进程模式。要求 ENTRY_FILE 指向一个 .php 脚本 |
旧的 INDEX_FILE 和 WORKER_FILE 变量仍会被解析(启动时会有一条 WARN)并映射到新模型上。参见 配置 → 已弃用。
路由模式
传统模式、框架模式和 SPA 模式在 WORKER_MODE_ENABLED=false 时由 ENTRY_FILE 选择,每种都对应一份等价的 nginx try_files 配置。
当 ENTRY_FILE 未设置(或为空)且 WORKER_MODE_ENABLED=false 时生效。等价的 nginx 配置:
location / {
try_files $uri $uri/ /index.php /index.html =404;
}
location ~ \.php$ {
try_files $uri =404; # PATH_INFO splitting enabled
}解析顺序:
$uri— 磁盘上的精确文件 → 直接提供(若为.php则执行)$uri/— 目录 → 在其中查找index.php,然后index.html- PATH_INFO 拆分 — 当 URI 包含
.php/时,脚本前缀会与磁盘匹配,剩余部分成为PATH_INFO(例如/api.php/users/42→ 脚本api.php,PATH_INFO=/users/42) /index.php— 根前端控制器回退/index.html— 根静态首页回退- 404
示例:
| 请求 | 结果 |
|---|---|
/about.php |
执行 about.php |
/style.css |
提供 style.css |
/blog/(存在 blog/index.php) |
执行 blog/index.php |
/api.php/users/42 |
执行 api.php,PATH_INFO=/users/42 |
/missing.txt |
回退到 /index.php |
/some/route |
回退到 /index.php |
在传统模式下,PATH_INFO 拆分始终开启。没有环境变量开关——此前的 SPLIT_PATH_INFO_ENABLED 标志已被移除。
当 ENTRY_FILE=index.php(或任何以 .php 结尾的值)且 WORKER_MODE_ENABLED=false 时生效。等价的 nginx 配置:
location ~ \.(?!php$)[a-zA-Z0-9]+$ {
try_files $uri /index.php; # static assets: fall back to front controller
}
location / {
rewrite ^ /index.php last; # everything else → front controller
}
location = /index.php {
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass ...;
}解析规则:
| URI 类别 | 行为 |
|---|---|
.css、.png、.js……(任何非 php 扩展名) |
若文件存在则提供,否则重写到 /index.php |
.php(任何路径) |
重写到 /index.php |
无扩展名(/api/users、/) |
重写到 /index.php |
仅当请求显式指定带尾部段的入口文件(/index.php/extra)时才会设置 PATH_INFO;对于应用路由,原始路径从 REQUEST_URI 读取。
示例:
| 请求 | 结果 | $_SERVER['PATH_INFO'] |
|---|---|---|
/style.css(存在) |
提供 style.css |
— |
/style.css(缺失) |
执行 index.php |
(不存在) |
/api/users |
执行 index.php |
(不存在) |
/about.php |
执行 index.php |
(不存在) |
/index.php/news/local |
执行 index.php |
/news/local |
/index.php(直接访问) |
执行 index.php |
(不存在) |
/ |
执行 index.php |
(不存在) |
对于应用路由,原始路径通过 REQUEST_URI 暴露,因此你的路由器读取 $_SERVER['REQUEST_URI'] 来进行派发。直接访问 /index.php 不再被拦截——重写是幂等的,因此直接访问它与访问 / 得到相同的结果。
缺失的静态资源会回退到前端控制器,而不是返回快速的 404——这正是 Laravel 和 Symfony 默认自带的 try_files $uri /index.php 行为,因此你的应用会为缺失的资源渲染自己的 404。代价是每个指向不存在资源的请求现在都会运行 PHP;如果前端控制器仍然没有内容可提供(/index.php 本身缺失),该请求会返回一个硬 404。
当 ENTRY_FILE=index.html(或任何非 .php 值)且 WORKER_MODE_ENABLED=false 时生效。等价的 nginx 配置:
location ~ \.php$ {
try_files $uri =404; # PHP: file must exist, no fallback
}
location ~ \. {
try_files $uri =404; # other extensions: hard 404 if missing
}
location / {
try_files /index.html =404; # no-extension paths: straight to index.html
}解析规则:
| URI 类别 | 行为 |
|---|---|
.php |
若文件存在则执行,否则硬 404 |
.css、.png……(任何其他扩展名) |
若文件存在则提供,否则硬 404 |
无扩展名(/dashboard、/api/users、/) |
直接提供 /index.html —— 不探测 $uri 的磁盘 |
示例:
| 请求 | 结果 |
|---|---|
/style.css(存在) |
提供 style.css |
/style.css(缺失) |
404 |
/dashboard |
提供 /index.html |
/users/42/edit |
提供 /index.html |
/api.php(存在) |
执行 api.php |
/api.php(缺失) |
404 |
/index.html(直接访问) |
提供 index.html |
有两点语义值得强调:
- 无扩展名路径跳过磁盘 —— SPA 模式从不追问「
/dashboard在磁盘上是否存在?」它总是返回首页。这对于客户端路由器是正确的,并避免了不必要的stat()调用。 - 缺失的静态文件是硬 404,而非贯穿回退 —— 缺失的
/style.css不会悄悄地提供index.html。这能尽早发现损坏的资源引用,而不是在 JS 期望 CSS 时返回 HTML。
由于 SPA 模式会直接执行已存在的 .php 文件,因此 PHP_DENY_PATHS 同样适用——用它来阻止在 /uploads 等可写目录内执行。
工作进程模式
工作进程模式在 WORKER_MODE_ENABLED=true 且 ENTRY_FILE 指向一个 .php 脚本时激活。路由器从磁盘提供静态资源,并把所有其他请求派发给工作进程的 ENTRY_FILE——该工作进程就是唯一的前端控制器。
| URI 类别 | 行为 |
|---|---|
静态资源(.css、.png……——任何非 .php 扩展名) |
若存在则直接从磁盘提供;缺失的资源会贯穿到工作进程的 ENTRY_FILE(而非硬 404) |
其他一切(.php URI、无扩展名路径、/) |
派发给工作进程的 ENTRY_FILE |
在工作进程模式下,文档根目录中任意的 .php 文件永不被直接执行——即使 about.php 在磁盘上存在,对 /about.php 的请求也会像任何其他路由一样到达工作进程回调。同样没有目录索引查找,也没有根 index.php 回退;这些请求都会由工作进程自己接收。
有两个例外,都是在模式派发之前运行的服务器级防御:点段路径(/.git/config、/.env、裸的 /.well-known)会被 点路径拦截 拒绝,而 /.well-known/ 下的 .php URI 会作为纵深防御被拒绝。两者都返回 404,永远不会到达工作进程。
启动时的校验会拒绝两种组合:
WORKER_MODE_ENABLED=true但未设置ENTRY_FILE→WORKER_MODE_ENABLED=true requires ENTRY_FILE to be set。WORKER_MODE_ENABLED=true但ENTRY_FILE为非.php→WORKER_MODE_ENABLED=true requires a .php ENTRY_FILE。
完整配置细节参见 工作进程模式。
PATH_INFO 行为
$_SERVER['PATH_INFO'] 的填充方式因模式而异:
| 模式 | 何时设置 | 值 |
|---|---|---|
| 传统模式 | 仅当 URI 包含 .php/ 时(PATH_INFO 拆分) |
脚本段之后的尾部,例如 /users/42 |
| 框架模式 | 仅对显式的 /index.php/extra 请求 |
入口文件之后的尾部,例如 /news |
| SPA 模式 | 从不 | (PHP 仅对精确的 .php 文件被调用;无 PATH_INFO) |
PATH_INFO 遵循 CGI 语义:只有当 SCRIPT_NAME(被执行的脚本)是请求路径的字面前缀时它才存在。一个 URL 未指名的前端控制器重写——应用路由、目录索引、静态未命中回退——都不携带 PATH_INFO;此时应改读 REQUEST_URI。在传统模式下,此前的 SPLIT_PATH_INFO_ENABLED 环境变量已被移除。
路径安全
OxPHP 应用多层保护来防止目录遍历、隐藏文件泄露和符号链接逃逸攻击:
- 百分号解码 在净化之前运行,因此像
/%2e%2e/etc/passwd这样的编码遍历尝试会被捕获 - 段过滤 从解析出的路径中移除
..、.和空段 - 符号链接校验 会规范化每个解析出的路径,并验证它仍在文档根目录之内。指向所提供目录之外的符号链接会被拦截
- 点路径拦截 会拦截任何以
.开头的路径段(例如/.git/config、/.env),依据 RFC 8615 对/.well-known/*除外 - well-known PHP 拦截 —— 即使有点路径的除外规则,
/.well-known/下的.php脚本也永不执行(纵深防御) - PHP 执行拒绝列表 —— 在直接映射模式(传统模式和 SPA 模式)下,
PHP_DENY_PATHS会在任何磁盘 I/O 之前,在配置的 glob 模式(例如/uploads/**,或像/admin/legacy.php这样的单个文件)处拦截.php执行。参见 PHP 执行拒绝列表
如果文档根目录在启动时不存在,服务器会以致命错误退出。符号链接逃逸保护需要一个有效、可解析的文档根路径。
故障排查
传统模式下所有请求都返回 404
检查文档根目录下是否存在 index.php 或 index.html。传统模式的 try_files 链只会回退到这两者——如果两者都缺失且没有文件匹配该 URL,你就会得到 404。
docker exec <container> ls /var/www/html/public缺失的静态资源返回 404 而不是 SPA 外壳
这在 SPA 模式下是刻意为之:缺失的 /style.css 是一个硬 404,而不是悄悄回退到 index.html,这能尽早发现损坏的资源引用。在框架模式和传统模式下,缺失的静态文件会回退到前端控制器(/index.php),因此由你的应用路由器渲染 404。如果你希望缺失资源返回硬 404,请使用 SPA 模式。
直接访问 /index.php 不再返回 404
在框架模式下,现在允许直接访问前端控制器(到 /index.php 的重写是幂等的)。如果你此前依赖 404 来检测直接访问,请改为在控制器内部检查 REQUEST_URI。
框架模式下 PATH_INFO 为空
这对于应用路由是预期行为。框架模式遵循 CGI 语义:只有当请求显式指定带尾部段的入口文件时(/index.php/news → /news)才会设置 PATH_INFO。对于像 /users/42 这样的普通应用路由,前端控制器是通过一次它未指名的内部重写到达的,因此 PATH_INFO 不存在——请从 $_SERVER['REQUEST_URI'] 读取路径。(如果 ENTRY_FILE 不以 .php 结尾,OxPHP 会选择 SPA 模式,而它从不填充 PATH_INFO。)
文档根目录内的符号链接返回 404
指向文档根目录之外的符号链接按设计会被拦截。请将目标内容移动到文档根目录之内,或将其作为目录挂载到正确的路径上。
Docker 示例
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
environment:
- DOCUMENT_ROOT=/var/www/html/public
- ENTRY_FILE=index.php