超全局变量
OxPHP 会在你的脚本执行之前填充好每一个标准 PHP 超全局变量,与 PHP 开发者在传统服务器环境下所期望的行为完全一致。这些值从你代码的第一行起就可用,无需任何初始化。只有一个设置会收窄这一点;参见禁用超全局变量。
$_SERVER
OxPHP 依照 CGI/1.1 规范,从传入的 HTTP 请求构建 $_SERVER。它会先导入进程的环境变量,再设置 CGI 变量,因此请求相关的值总是会覆盖任何与之冲突的环境变量键。
标准变量
| 变量 | 说明 | 示例 |
|---|---|---|
SCRIPT_FILENAME |
正在执行的 PHP 脚本的绝对文件系统路径 | /var/www/html/public/index.php |
DOCUMENT_ROOT |
通过 DOCUMENT_ROOT 环境变量配置的 Web 根目录 |
/var/www/html/public |
SERVER_SOFTWARE |
服务器标识(包含正在运行的 OxPHP 版本) | OxPHP/0.13.0 |
SERVER_PROTOCOL |
协商得到的 HTTP 协议版本 | HTTP/2 |
REQUEST_METHOD |
HTTP 方法 | GET |
REQUEST_URI |
包含查询字符串的完整 URI | /app?page=2 |
SCRIPT_NAME |
所执行脚本相对于 DOCUMENT_ROOT 的路径——在 Framework 模式下是前端控制器,而非请求 URI |
/index.php |
DOCUMENT_URI |
SCRIPT_NAME 的别名,用于兼容 nginx/PHP-FPM |
/index.php |
PHP_SELF |
存在 PATH_INFO 时为 SCRIPT_NAME 加上 PATH_INFO,否则等于 SCRIPT_NAME |
/index.php/user/42 |
QUERY_STRING |
URI 的查询部分(不存在时为空字符串) | page=2 |
SERVER_NAME |
来自 Host 请求头的主机名 |
example.com |
SERVER_PORT |
来自 Host 请求头的端口 |
8080 |
REMOTE_ADDR |
客户端 IP 地址 | 172.17.0.1 |
REMOTE_PORT |
客户端端口号 | 54321 |
HTTPS |
连接使用 TLS 时设为 "on",否则不存在 |
on |
REQUEST_SCHEME |
TLS 连接为 "https",否则为 "http" |
https |
CONTENT_TYPE |
Content-Type 请求头的值(无 HTTP_ 前缀) |
application/json |
CONTENT_LENGTH |
Content-Length 请求头的值(无 HTTP_ 前缀) |
128 |
REQUEST_TIME |
请求开始时的 Unix 时间戳(整数) | 1738800000 |
REQUEST_TIME_FLOAT |
精确到微秒的 Unix 时间戳 | 1738800000.123456 |
GATEWAY_INTERFACE |
CGI 版本字符串 | CGI/1.1 |
当 Host 请求头缺失时,SERVER_NAME 默认为 localhost,SERVER_PORT 默认为 80(TLS 下为 443)。
HTTP 请求头
所有 HTTP 请求头都会以 HTTP_ 前缀添加到 $_SERVER 中。请求头名称会按照 CGI/1.1 约定转换为大写,并将短横线替换为下划线:
Accept: text/html -> HTTP_ACCEPT
X-Forwarded-For: 1.2.3.4 -> HTTP_X_FORWARDED_FOR
Authorization: Bearer abc -> HTTP_AUTHORIZATION
Cookie: session=xyz -> HTTP_COOKIEContent-Type 和 Content-Length 不带 HTTP_ 前缀——分别为 CONTENT_TYPE 和 CONTENT_LENGTH——这是 CGI 规范的要求。
反向代理之后
当配置了 TRUSTED_PROXIES 且请求的对端处于可信集合中时,OxPHP 会从转发头(X-Forwarded-* 或 RFC 7239 Forwarded)重写以下 $_SERVER 键:
| 变量 | 对端可信时的值 | 其他情况下的值 |
|---|---|---|
REMOTE_ADDR |
来自 X-Forwarded-For / Forwarded 中最右侧的不可信地址;当 TRUSTED_PROXIES 中包含 cloudflare 时,转发链显示来自 Cloudflare 边缘节点的请求取 CF-Connecting-IP;转发请求头得不出地址时为直连对端 |
直连对端 IP |
REMOTE_PORT |
来自 Forwarded 或 X-Forwarded-For 中 ip:port 条目的客户端源端口,否则为 0;地址未被重写时为对端的端口 |
直连对端端口 |
HTTPS |
当 X-Forwarded-Proto: https 时为 "on" |
仅当对端连接为 TLS 时设置 |
REQUEST_SCHEME |
来自 X-Forwarded-Proto 的 "https" / "http" |
基于实际 TLS 状态 |
SERVER_NAME |
X-Forwarded-Host 的主机部分 |
Host 请求头的主机部分 |
SERVER_PORT |
X-Forwarded-Port,否则取 X-Forwarded-Host 的端口部分,否则按 scheme 取 443/80 |
Host 的端口部分,或 443/80 |
原始的 HTTP_X_FORWARDED_FOR、HTTP_X_FORWARDED_PROTO、HTTP_X_FORWARDED_HOST、HTTP_X_FORWARDED_PORT 和 HTTP_FORWARDED 键会原封不动地保留在 $_SERVER 中——重写后的值和原始请求头都可以获取。
当客户端地址被重写时,如果地址所来自的条目携带了端口(RFC 7239 的 Forwarded: for="ip:port" 节点,或 X-Forwarded-For 中的 ip:port 条目),REMOTE_PORT 就是客户端的源端口,否则为 "0":这个值会被置零而不是靠猜测填充。取自 CF-Connecting-IP 的地址同样不携带端口,所以此时即使 Forwarded 中带有端口,REMOTE_PORT 也为 "0"。当转发请求头得不出客户端地址时(参见客户端 IP 提取),REMOTE_ADDR 和 REMOTE_PORT 都不会被重写:二者都保持为直连对端的值。
当未设置 TRUSTED_PROXIES 时,不会发生任何重写,REMOTE_ADDR 始终是直连对端——通常是你的负载均衡器,而非最终客户端。手动解析 X-Forwarded-For 容易出错(最左侧还是最右侧、缺少 CIDR 信任校验);建议配置 TRUSTED_PROXIES。信任算法和配置语法请参阅 可信代理。
追踪上下文变量
启用分布式追踪后,OxPHP 会向 $_SERVER 添加追踪上下文变量:
| 变量 | 说明 | 示例 |
|---|---|---|
OXPHP_TRACE_ID |
当前请求的 W3C 追踪 ID | 4bf92f3577b34da6a3ce929d0e0e4736 |
OXPHP_SPAN_ID |
OxPHP 服务器 span 的 Span ID | 00f067aa0ba902b7 |
OXPHP_PARENT_SPAN_ID |
来自上游服务的父 span ID(若为根则为空) | b9c7c989f97918e1 |
只有在收到有效的 traceparent 请求头,或 OxPHP 生成了一个新的 trace 时,这些变量才会存在。如果未配置追踪,这些键将不存在。
与 PHP-FPM 的差异
以下变量的行为与标准 PHP-FPM 环境不同:
| 变量 | 行为 |
|---|---|
SERVER_ADDR |
不设置。OxPHP 不填充本地服务器 IP 地址。 |
PATH_INFO |
自动设置——参见下文 PATH_INFO 行为。 |
PATH_TRANSLATED |
不设置。 |
PHP_AUTH_USER / PHP_AUTH_PW / AUTH_TYPE |
不从 Authorization 请求头中提取。请直接读取 $_SERVER['HTTP_AUTHORIZATION']。 |
REDIRECT_STATUS |
不设置。OxPHP 不使用内部重定向机制。 |
示例
<?php
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$ip = $_SERVER['REMOTE_ADDR'];
$host = $_SERVER['SERVER_NAME'];
$scheme = $_SERVER['REQUEST_SCHEME']; // "http" or "https"
// Read a custom header
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
// REMOTE_ADDR is already the real client IP when TRUSTED_PROXIES is configured.
// Without it, REMOTE_ADDR is the direct peer (usually a load balancer).
$clientIp = $_SERVER['REMOTE_ADDR'];
// Check TLS without checking the port
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
// Secure connection
}PATH_INFO 行为
$_SERVER['PATH_INFO'] 会根据当前生效的路由模式自动填充。这里没有功能开关——此前的 SPLIT_PATH_INFO_ENABLED 环境变量已被移除。
| 路由模式 | 何时设置 | 值 |
|---|---|---|
Traditional(未设置 ENTRY_FILE) |
仅当 URI 包含 .php/ 且脚本前缀在磁盘上存在时 |
脚本段之后的尾部 |
Framework(ENTRY_FILE=index.php) |
仅当请求显式指定入口文件并带有尾部段时(/index.php/extra) |
入口文件之后的尾部,例如 /news |
SPA(ENTRY_FILE=index.html) |
永不设置——PHP 只为精确匹配的 .php 文件运行,无 PATH_INFO |
— |
SCRIPT_NAME 始终标识被执行的脚本(相对于文档根目录解析出的文件),因此在正常路由下,只有当 SCRIPT_NAME 是请求路径的字面前缀时,PATH_INFO 才存在。当请求被重写到一个它并未指名的前端控制器时(应用路由、目录索引、静态文件未命中回退),PATH_INFO 缺失,原始路径存在于 REQUEST_URI 中。(PHP_DENY_PATHS 回退是一个有意的例外:它会将 PATH_INFO 设为原始经清理的 URI,以便回退脚本可以据此路由。)
Traditional 模式示例
OxPHP 从左到右扫描 URI,寻找第一个对应磁盘上真实文件的 .php 段。其后的所有内容都会成为 PATH_INFO:
| 请求 URI | 磁盘上的文件 | SCRIPT_NAME |
PATH_INFO |
PHP_SELF |
|---|---|---|---|---|
/app.php/user/42 |
app.php 存在 |
/app.php |
/user/42 |
/app.php/user/42 |
/index.php/api/v2/users |
index.php 存在 |
/index.php |
/api/v2/users |
/index.php/api/v2/users |
/app.php |
app.php 存在 |
/app.php |
(不存在) | /app.php |
/missing.php/foo |
文件未找到 | 回退到 /index.php |
— | 取决于回退情况 |
Framework 模式示例
每个非静态请求都会被重写到 index.php 上。只有当请求显式指定入口文件并带有尾部段时,PATH_INFO 才会被设置;对于应用路由,原始路径从 REQUEST_URI 读取。
| 请求 URI | SCRIPT_NAME |
PATH_INFO |
|---|---|---|
/api/users |
/index.php |
(不存在) |
/about.php |
/index.php |
(不存在) |
/index.php/news/local |
/index.php |
/news/local |
/index.php |
/index.php |
(不存在) |
PATH_TRANSLATED 不会被填充。它在实践中很少使用,nginx 和 PHP-FPM 默认也不设置它。
$_GET
查询字符串参数会自动从请求 URI 中解析。
<?php
// Request: GET /search?q=oxphp&page=2
$query = $_GET['q']; // "oxphp"
$page = $_GET['page']; // "2"数组语法也能如预期般工作:
<?php
// Request: GET /filter?tags[]=php&tags[]=async
$tags = $_GET['tags']; // ["php", "async"]filter_input()、filter_input_array() 和 filter_has_var() 读的不是 $_GET、$_POST 或 $_COOKIE。filter 扩展自己保存着一份已解析输入的副本,而在持久化工作进程中,这份副本会被每个请求填充——因此它也必须被每个请求交还,否则一个客户端的查询值、会话 cookie 和请求体字段,会一直可被该工作进程之后服务的每个请求读到。OxPHP 会在每个请求开始时把它交还,所以这三个函数只为发问的那个请求作答,不为任何其他请求作答。
工作进程模式增加了一条限制。如果你的请求暂停了(sleep()、一次 await、一个被钩住的调用),而在这个窗口里有另一个请求在该工作进程上运行,那么这份存储就变成了那个请求的。恢复之后的读取会得到 null,而不是别人的输入,你的请求原本那份副本到那时也已经没了。另一个请求只要在那里开始或恢复就足够了——不需要跑完。$_GET、$_POST 和 $_COOKIE 会随请求跨越挂起、不受影响,所以要么读它们,要么在挂起之前读 filter 函数。
有一个调用完全不允许暂停。传入按字段定义数组的 filter_input_array() 会对每个字段读一次该存储,因此做 I/O 的 FILTER_CALLBACK 会在其持续期间占住工作进程,而不是把它交给另一个请求;在这样的回调里显式调用 Fiber::suspend() 会抛出异常。等待仍然会发生——等待的是工作线程而不是请求——所以慢回调会表现为排在它后面所有请求的延迟,而那些排队的请求一旦超出各自的等待预算就会被卸除。它能持续多久由回调正在与之通信的对象决定,而不是由服务器决定:流封装器在 default_socket_timeout 后放弃(默认 60 秒),mysqlnd 则会等 mysqlnd.net_read_timeout(默认长达一天)。请给这类调用一个显式超时——或者更好:等 filter_input_array() 把值返回之后,再对它运行需要访问数据库或缓存的校验。
$_POST
OxPHP 支持表单提交的两种标准内容类型:
application/x-www-form-urlencoded—— 标准 HTML 表单数据multipart/form-data—— 文件上传与表单字段的组合
<?php
// Request: POST /login
// Content-Type: application/x-www-form-urlencoded
// Body: username=admin&password=secret
$username = $_POST['username']; // "admin"
$password = $_POST['password']; // "secret"对于 JSON 或其他内容类型,请改用 php://input:
<?php
// Request: POST /api/users
// Content-Type: application/json
// Body: {"name":"Alice","email":"alice@example.com"}
$data = json_decode(file_get_contents('php://input'), true);
$name = $data['name']; // "Alice"
$email = $data['email']; // "alice@example.com"filter_input(INPUT_POST, …) 读取的是 filter 扩展自己那份请求体副本,而不是 $_POST——那份副本是什么、能存活多久,参见 $_GET 下的说明。
$_COOKIE
Cookie 会从 Cookie 请求头中解析。
<?php
// Request with: Cookie: session=abc123; theme=dark
$session = $_COOKIE['session']; // "abc123"
$theme = $_COOKIE['theme']; // "dark"带有 __oxp_ 前缀的 Cookie 保留给 OxPHP 内部插件使用。它们会在 Cookie 请求头到达 PHP 之前被剥离,不会出现在 $_COOKIE 中。
filter_input(INPUT_COOKIE, …) 读取的是 filter 扩展自己那份 cookie 副本,而不是 $_COOKIE——那份副本是什么、能存活多久,参见 $_GET 下的说明。
$_FILES
通过 multipart/form-data 发送的文件上传会以标准 PHP 结构填充 $_FILES 数组:
<?php
// $_FILES['avatar'] structure:
// [
// 'name' => 'photo.jpg', // Original filename sent by the client
// 'type' => 'image/jpeg', // MIME type declared by the client
// 'tmp_name' => '/tmp/phpAb12Cd', // Temporary file path on the server
// 'error' => 0, // UPLOAD_ERR_OK (0 means no error)
// 'size' => 204800, // File size in bytes
// ]
if ($_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
$tmp = $_FILES['avatar']['tmp_name'];
$name = basename($_FILES['avatar']['name']);
move_uploaded_file($tmp, "/uploads/$name");
}$_REQUEST
$_REQUEST 是 $_GET、$_POST 和 $_COOKIE 合并后的数组,由 PHP 根据 request_order INI 指令构建;未设置 request_order 时则依据 variables_order。合并本身完全遵循 PHP 的规则,不作改动:各来源按指令列出的顺序合并,后面的来源会覆盖前面的来源已设置的同名键。
OxPHP 镜像不加载任何 php.ini:php.ini-production 和 php.ini-development 放在 /usr/local/etc/php 里,但并未启用(参见安装)。因此 request_order 未设置,PHP 会回退到其内置的 variables_order,即 "EGPCS":先 GET,再 POST,最后 COOKIE。于是同名的 cookie 会替换查询参数或请求体中的值;当两边都是数组时(请求体里有 k[a],cookie 里有 k[b]),它们会按同样的规则逐键合并。
你在 PHP-FPM 环境中可能见过的 request_order = "GP",来自 php.ini-production 和 php.ini-development 这两个示例文件,而不是 PHP 本身。要把 cookie 排除在外,请在启动时加载的 ini 文件中设置它,例如创建 /usr/local/etc/php/conf.d/zz-request-order.ini,内容为 request_order = "GP"。ini_set() 无法在运行时修改这条指令。
<?php
// POST /form?action=preview with body action=submit
$action = $_REQUEST['action']; // "submit" — POST overrides GET
// Same request, also carrying the header "Cookie: action=delete"
$action = $_REQUEST['action']; // "delete" — the cookie overrides both$_REQUEST 会为每个请求重建。PHP 通常是惰性构建它的——在提到它的脚本第一次加载时构建一次——这在持久化工作进程中意味着之后的每个请求读到的都是第一个请求的参数。OxPHP 强制重建,因此这个合并数组描述的始终是正在被服务的那个请求。
$_ENV
$_ENV 保存进程环境。在传统模式下,它的行为与 PHP-FPM 完全一致:PHP 会在每个请求上依照 variables_order 从环境重新填充它。
**工作进程模式会把它固定下来。**工作进程只引导一次,而 .env 加载器(vlucas/phpdotenv、symfony/dotenv、Laravel 的 Env)会把它们的值直接写进 $_ENV,不触碰进程环境。若按请求重新填充 $_ENV,应用的配置从第二个请求起就会被抹掉,所以在工作进程模式下,这个数组一旦存在,就会在工作进程的整个生命期内保留。你的引导代码写进去的内容对该工作进程服务的每个请求都保持可见,其他任何写入也一样;示例下方的警告说明了这在请求处理函数中意味着什么。
<?php
// bootstrap, before oxphp_worker()
Dotenv\Dotenv::createImmutable(__DIR__)->load(); // writes into $_ENV
oxphp_worker(function () {
echo $_ENV['DATABASE_URL']; // still there on request 10_000
});这一机制只是把数组固定下来,因此它不区分引导代码的写入和处理函数的写入:在请求处理函数里执行 $_ENV['tenant_id'] = $request->tenant();,这个值会在该工作进程余下的整个生命期内留在 $_ENV 中。该工作进程之后服务的每个请求都会读到它。当写入它的请求停在某个挂起点时,工作进程接手的请求同样会读到它,因为 $_ENV 被有意排除在挂起请求随身携带的状态之外,两个请求于是读的是同一个数组。这两种情况都不会有任何提示。不会产生警告,也没有日志记录;一个请求只是读到了另一个请求设置的值。
只在引导代码里写 $_ENV,别的地方都不要写。按请求变化的值,应当存放在每个请求都会重建的地方。$_SERVER 就是这样,而且它会随自己的请求跨越挂起。框架限定在请求作用域内的任何东西也可以,但要确认它确实限定在请求上,而不是工作进程上;在引导阶段只设置一次的容器绑定或请求栈属于工作进程状态,影响范围和 $_ENV 一样。OxPHP 自己的请求对象上的属性容器也可以,前提是你把这个对象传递下去:oxphp_http_request() 每次调用都会构建一个新的 Request,随之带来一个新的空容器。
在工作进程模式下,filter_input(INPUT_ENV, …)、filter_input_array(INPUT_ENV) 和 filter_has_var(INPUT_ENV, …) 读的是进程环境。它们不读 $_ENV,所以写进去的值——无论出自你的引导代码还是别处——都不在其中。PHP 在任何地方都是这样表现的,因为写入 $_ENV 会让这个数组拥有自己的副本,而 filter 扩展继续读引擎当初拍下的环境快照。工作进程模式加了一个细节:这份快照是 $_ENV 在该工作进程上第一次被构建时拍下的,所以之后的 putenv() 调用也不在里面。getenv() 读的是实时环境,不受影响;$_ENV 保存的是进程值加上该工作进程上的 PHP 写进去的任何内容。以上只针对 INPUT_ENV:INPUT_GET、INPUT_POST 和 INPUT_COOKIE 读的是 filter 扩展内部它们自己的存储,见上文 $_GET 下的说明。
这种固定依赖 PHP 默认的 auto_globals_jit=1。在 auto_globals_jit=0 时,PHP 会在任何扩展来得及介入之前,于每个请求上从进程环境重新填充 $_ENV,.env 加载器的值在工作进程模式下将无法存活。
php://input
原始请求体可通过 php://input 流获取。这是读取 JSON 负载(请求体)、XML 或除表单提交之外任何内容类型的标准方式。
<?php
$body = file_get_contents('php://input');
$data = json_decode($body, true);php://input 可倒回(rewindable),在同一请求内可多次读取。
对于 multipart/form-data 请求,php://input 为空。这类请求请使用 $_POST 和 $_FILES。
禁用超全局变量
设置 SUPERGLOBALS_ENABLED=false 后,OxPHP 将不再向 PHP 描述请求:CGI 变量和 HTTP_* 变量不会被注册,查询字符串也不会交给 PHP 解析。请改用 HTTP 请求 API(oxphp_http_request())来访问请求数据。
SUPERGLOBALS_ENABLED=false # the $_SERVER request keys and $_GET are not built它影响到的数组比名字暗示的要少。请求体和 Cookie 请求头无论如何都会交给 PHP,所以 PHP 由它们构建出的一切都照常存在:
| 内容 | SUPERGLOBALS_ENABLED=false 时 |
|---|---|
$_GET |
为空 |
$_SERVER |
请求期间只有四个键:REQUEST_TIME、REQUEST_TIME_FLOAT、argc 和 argv,全部由 PHP 自身注册,而非服务器。REQUEST_METHOD、REQUEST_URI、HTTP_* 键以及进程环境都不存在。工作进程模式的引导阶段有所不同:oxphp_worker() 之上的代码在任何请求之前运行,看到的 $_SERVER 就像该设置处于开启状态时那样构建,包含完整的进程环境,外加一个占位的 REQUEST_URI,值为 / |
$_POST、$_FILES、$_COOKIE |
照常填充 |
$_REQUEST |
照常填充,一如既往由 PHP 合并(参见 $_REQUEST);只是缺少本应来自 $_GET 的部分 |
$_SESSION |
当请求启动会话时,由 PHP 的会话模块填充,而非 SAPI |
php://input |
是一个流,而非超全局变量;照常可读 |
header()、headers_list() 等 |
SAPI 函数,而非超全局变量 |
session_start() 及其他 session_*() 函数 |
原生 PHP 函数 |
oxphp_http_request() |
可用;每个方法都会给出结果,包括 query() 和 payload() |
依据 $_SERVER['REQUEST_URI'] 做路由的代码在此设置下什么也拿不到,必须改为读取 oxphp_http_request()->path()。在工作进程入口脚本中,这适用于处理函数。处理函数之上的引导代码看到的是表中所述的占位值 /,那也不是真实的路由。
你可以在运行时检查当前设置:
if (!oxphp_superglobals_enabled()) {
$request = oxphp_http_request();
$page = $request->query('page', 1);
}给你的智能体的提示词
这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。
审查我的代码里对超全局变量的读取,看看 OxPHP 和 PHP-FPM 之间有哪些差异。通过 OxPHP 文档 MCP 服务器读取 `php/superglobals` 页面。
在我的应用里找出对 `$_SERVER['PHP_AUTH_USER']`、`SERVER_ADDR` 或 `PATH_TRANSLATED` 的读取,以及那些指望它们已被预先解析好的 Basic-Auth 代码,然后告诉我哪些读取需要改、改成什么。我的应用在 OxPHP 工作进程模式下,会丢掉它通过 `filter_input()` 读到的值。通过 OxPHP 文档 MCP 服务器读取 `php/superglobals` 页面。
检查我的代码里那些会因 I/O 或纤程挂起而暂停的 `filter_input()` 和 `filter_input_array()` 调用,告诉我哪些暴露在页面所讲的工作进程模式竞态之下——恢复之后的请求读到的是另一个请求的那份存储。另请参阅
- HTTP 请求 API —— 类型化、惰性加载的请求对象,作为超全局变量的替代方案
- PHP 函数 ——
oxphp_request_id()、oxphp_worker_id()及其他扩展函数 - 工作进程模式 —— 工作进程各次请求之间如何刷新超全局变量
- 配置参考 ——
DOCUMENT_ROOT及其他服务器配置变量