HTTP 请求 API
OxPHP 提供了一个面向对象的 API 来访问 HTTP 请求数据。你不再直接读取 $_GET、$_POST、$_COOKIE、$_FILES 和 $_SERVER,而是调用 Request 对象上的方法,它只返回你所请求的内容,不多也不少。
目录
- 概述
- 获取 Request 对象
- RequestInterface 方法
- SessionInterface
- UploadedFileInterface
- AttributesInterface
- 异常
- SUPERGLOBALS_ENABLED
- 工作进程模式
- IDE 支持
- 示例
概述
oxphp_http_request() 返回一个只读代理,指向存储在当前工作线程中的 HTTP 请求数据。数据是延迟获取的。像 $request->header('Accept') 这样的单次方法调用会直接访问 Rust 端的数据结构,只返回那一个值。在返回整个数组的调用中,query() 和 payload() 会把解析结果保存在被调用的那个对象上;headers()、cookies() 和 files() 则每次都重新构建。没有任何东西是按请求保存的:每次调用 oxphp_http_request() 都会返回一个新对象,连同一套新的上述缓存。
为什么用它而不是超全局变量?
- 内置 JSON 请求体解析。 无论请求方法是什么,
$request->payload()都会返回解码后的application/json请求体;对于POST表单请求体(application/x-www-form-urlencoded或multipart/form-data),它返回其中的字段,无需额外代码。确切的返回值见 解析后的请求体。 - 不会写错数组键。
$request->method()比$_SERVER['REQUEST_METHOD']更不容易拼错。 - 文件上传类型自动检测。
$request->file('avatar')->type()返回的 MIME 类型是根据文件的实际内容确定的,而非客户端提供的值。 - 可测试。 由于行为由接口定义,你可以在单元测试中注入 mock 实现。
- 超全局变量仍然可用。 设置
SUPERGLOBALS_ENABLED=false是可选的。无论如何,对象 API 都能正常工作。
获取 Request 对象
<?php
$request = oxphp_http_request();你可以在处于活动 HTTP 请求中执行的脚本的任何位置调用 oxphp_http_request(),包括在 oxphp_worker() 回调内部:
<?php
oxphp_worker(function () {
$request = oxphp_http_request();
$method = $request->method();
// ...
});RequestInterface 方法
URI 与方法
$request->method(): string以大写形式返回 HTTP 方法:"GET"、"POST"、"PUT"、"PATCH"、"DELETE" 等。
$request->isMethod(string $method): bool不区分大小写的方法检查。
$request->path(): string不含查询字符串的 URI 路径:"/users/42"。
$request->fullUri(): string完整的 URI,包含协议(scheme)、主机、可选的非标准端口、路径和查询字符串:"https://example.com:8080/users/42?page=2"。标准端口(HTTP 为 80,HTTPS 为 443)会被省略。
$request->scheme(): string"https" 或 "http"。
$request->isSecure(): bool当协议为 "https" 时返回 true。
$request->host(): string来自 Host 请求头的主机名。当该请求头缺失时(不带 Host 请求头的 HTTP/1.0 请求)返回空字符串。
$request->port(): int来自 Host 请求头的端口。当未显式指定时,返回对应协议的默认端口:HTTP 为 80,HTTPS 为 443。
当 TRUSTED_PROXIES 包含对端时,scheme()、isSecure()、host() 和 port() 会遵循 X-Forwarded-Proto 和 X-Forwarded-Host。没有可信代理时,它们反映的是直接连接的信息。
$request->queryString(): ?string不含开头 ? 的原始查询字符串。当没有查询字符串时返回 null。
协议
$request->httpProtocol(): string完整的协议字符串:"HTTP/1.1" 或 "HTTP/2"。
$request->httpProtocolVersion(): string仅版本号:"1.1" 或 "2"。
查询参数
$request->query(?string $key = null, mixed $default = null): mixed访问查询字符串参数。
| 调用 | 返回 |
|---|---|
$request->query() |
所有参数组成的扁平名值对数组 |
$request->query('page') |
page 的值,若不存在则为 null |
$request->query('page', 1) |
page 的值,若不存在则为 1 |
方括号表示法目前还不会被展开为嵌套数组。?tags[]=php&tags[]=async 现在得到的是一个名为 tags[] 的扁平条目,而不是一个 tags 数组;重复出现的名字通过 query('name') 解析到第一次出现的值,而 query() 保留最后一次的。在此问题修复之前,数组参数请使用 $_GET。
// Request: GET /search?q=oxphp&tags[]=php&tags[]=async
$q = $request->query('q'); // "oxphp"
$tags = $_GET['tags']; // ["php", "async"]找到的值始终是字符串。当键不存在时,$default 会原样返回。不带值发送的参数(?flag)和发送为空的参数(?flag=)读到的都是 ''——只有不存在的参数才给出 null。
解码
名字和值都会做百分号解码,+ 被读作空格——与 PHP 构建 $_GET 时应用的规则相同:
// Request: GET /search?q=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82&tag=a+b
$request->query('q'); // "Привет"
$request->query('tag'); // "a b"值是字节串,因此一个编码了非法 UTF-8 内容的转义序列会原样保留——签名令牌或二进制 id 仍然可以按发送时的字节进行校验。queryString() 仍然是按到达原样、不解码地读取查询字符串的方式。
名字按客户端发送的样子报告,而 $_GET 按 PHP 归档它们的样子报告。PHP 会重写超全局变量名中的 ' '、'.' 和 '[',所以 ?a.b=1 落到 $_GET 里成了 a_b:
// Request: GET /?a.b=1
$_GET['a_b']; // "1"
$request->query('a.b'); // "1"在 PHP 会截断名字的地方,名字同样被完整保留。PHP 把超全局变量名当作 C 字符串处理,所以 ?a%00b=1 会变成 $_GET['a'],而 query() 保留 a\0b。不要把这当作“用一个 API 校验、用另一个 API 读取”的理由:这种截断会让一个参数落到另一个参数的键上,因此 ?a=1&a%00x=2 得到 $_GET['a'] === '2' 而 query('a') === '1'——两者不只是在名字集合上有分歧,连值都不一致。
最多解析 1000 个参数。PHP 有自己的限制 max_input_vars,默认同样是 1000 但可以配置——调高它之后,$_GET 会保留超出 query() 停止解析位置的参数。
解析后的请求体
$request->payload(?string $key = null, mixed $default = null): mixed返回解析后的请求体。请求体会根据 Content-Type 请求头进行解析:
| Content-Type | 返回 |
|---|---|
application/x-www-form-urlencoded |
POST 请求时为字段值组成的关联数组;其他任何方法时为空数组 |
multipart/form-data |
POST 请求时为文本字段值组成的关联数组;其他任何方法时为空数组 |
application/json |
解码后的值:对象或数组解码为数组,否则为标量本身(string、int、float、bool);无效 JSON 以及字面量 null 请求体返回 null |
| 其他任何值 | null。匹配按前缀进行,因此任何以 application/json 开头的值(例如 application/json-patch+json)都走上面的 JSON 行,而 text/json 和 application/vnd.api+json 落在这一行 |
无论请求方法是什么,JSON 请求体都会被解码。表单请求体则不然:两个表单行读取的是 PHP 解析请求体得到的结果,而 PHP 只对 POST 做这种解析,因此在携带表单字段的 PUT 或 PATCH 上,payload() 返回空数组。请把这类请求体以 JSON 发送,或者从 body() 读取后自行解析。
对于两种表单类型,payload() 返回的是 PHP 从请求体中解析出的字段。之后对 $_POST 的写入只会改变脚本自己的那份数组副本,payload() 看不到它。
解析结果会在首次调用时缓存,并由同一对象上的后续调用复用。每次 oxphp_http_request() 调用都会返回一个拥有自己缓存的独立对象,因此第二个对象会重新构建结果。
| 调用 | 返回 |
|---|---|
$request->payload() |
整个解析后的请求体 |
$request->payload(null, []) |
整个解析后的请求体;没有时为 []:空请求体、上表最后一行中的 Content-Type、无效 JSON,或字面量 JSON null |
$request->payload('email') |
单个字段的值,若不存在则为 null |
$request->payload('email', '') |
单个字段的值,若不存在则为 '' |
按键查找只会深入数组形式的负载。JSON 请求体解码为标量时没有键,因此 payload('anything') 返回默认值。解码为 false 或 0 的请求体会原样返回该值,默认值不会替代它。
<?php
// JSON request: POST /api/users
// Content-Type: application/json
// Body: {"name": "Alice", "role": "admin"}
$name = $request->payload('name'); // "Alice"
$role = $request->payload('role'); // "admin"
$data = $request->payload(); // ["name" => "Alice", "role" => "admin"]请求头
$request->header(string $name, ?string $default = null): ?string返回原始的请求头值。请求头名称不区分大小写。对于多值请求头(Accept、X-Forwarded-For),整行请求头会作为单个字符串返回。解析由你自行负责。
$request->hasHeader(string $name): bool如果指定的请求头存在,返回 true。
$request->headers(): array以关联数组的形式返回所有请求头。每个键是接收到的原始请求头名称(不做规范化),每个值是原始的请求头字符串。
<?php
$accept = $request->header('Accept');
// "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
if ($request->hasHeader('Authorization')) {
$token = $request->header('Authorization');
}
$all = $request->headers();
// ["Content-Type" => "application/json", "Accept" => "...", ...]Cookies
$request->cookie(string $name, ?string $default = null): ?string返回单个 cookie 的值,若该 cookie 不存在则返回 $default。发送为空的 cookie(Cookie: a=)读到的是 '';只有不存在的 cookie 才给出 $default。
$request->cookies(): array以名值对的关联数组形式返回所有 cookie。
解码
Cookie 的解码方式与查询参数不同,遵循 PHP 构建 $_COOKIE 时的做法:值会做百分号解码,但 + 保持为字面的 +,名字则完全不解码。
// Cookie: token=a%20b; sep=a+b; %D0%BA=1
$request->cookie('token'); // "a b" — percent-escapes decoded
$request->cookie('sep'); // "a+b" — unlike query(), "+" is not a space
$request->cookie('%D0%BA'); // "1" — look up by the name as sent这里的名字同样按发送原样报告。PHP 会重写每个超全局变量名中的 ' '、'.' 和 '[',cookie 也不例外,所以 Cookie: a.b=1 落到 $_COOKIE 里成了 a_b,而 cookie('a.b') 和 cookies()['a.b'] 使用客户端发送的名字。
值是字节串,因此签名的 cookie 或 setcookie('sid', random_bytes(16)) 都能以设置时的确切字节往返。
<?php
$theme = $request->cookie('theme', 'light'); // "dark" or "light"
$session = $request->cookie('session'); // null if absent
$all = $request->cookies(); // ["theme" => "dark", ...]原始请求体
$request->body(): string返回原始的请求体字节。这是 OxPHP 中等价于 file_get_contents('php://input') 的方式。与 payload() 不同,body() 不会被缓存。每次调用都会访问底层数据结构。
$request->contentType(): ?string返回 Content-Type 请求头的值,若不存在则返回 null。
<?php
// Read raw body for signature verification
$raw = $request->body();
$signature = $request->header('X-Hub-Signature-256');
$valid = hash_hmac('sha256', $raw, $secret) === $signature;body() 和 payload() 相互独立。你可以在同一个请求中同时调用两者。
文件上传
$request->file(string $name): ?UploadedFileInterface返回给定字段名对应的上传文件,若该字段不存在则返回 null。对于数组字段(name="photos[]"),返回第一个文件。
$request->files(?string $name = null): array| 调用 | 返回 |
|---|---|
$request->files() |
所有上传文件,作为 UploadedFileInterface 的扁平数组 |
$request->files('photos') |
photos 字段的所有文件(支持 name="photos[]") |
两者每次调用都会构建新的 UploadedFileInterface 对象。两次 file('avatar') 调用会为同一个上传文件返回两个对象,其中一个通过 type() 检测出的类型,另一个并不知道。如果需要多次使用,请保留拿到的那个对象,就像下面的示例那样。
<?php
$avatar = $request->file('avatar');
if ($avatar && $avatar->isValid()) {
$mime = $avatar->type(); // Detected from file contents, not the client claim
$name = $avatar->name(); // Original filename
$avatar->moveTo('/var/uploads/' . basename($name));
}
// Multiple files
$photos = $request->files('photos'); // UploadedFileInterface[]
foreach ($photos as $photo) {
if ($photo->isValid()) {
$photo->moveTo('/var/uploads/' . basename($photo->name()));
}
}客户端
$request->ip(): string返回客户端 IP 地址。当配置了 TRUSTED_PROXIES 且请求对端在可信集合中时,这是 X-Forwarded-For 或 RFC 7239 Forwarded 中最右侧的不可信地址。当 TRUSTED_PROXIES 指定了 cloudflare,且转发链表明请求来自 Cloudflare 边缘节点时,它则是 CF-Connecting-IP 中的地址。否则它是直接对端的 IP,通常是你的负载均衡器,而非最终客户端。
对于高级场景,原始的 X-Forwarded-For 请求头仍可通过 $request->header('X-Forwarded-For') 获取,但手动解析它很少能做对(最左还是最右、没有 CIDR 信任检查)。请改为配置 TRUSTED_PROXIES。参见 可信代理。
计时
$request->startTime(bool $asFloat = false): int|float返回接收到此请求时的 Unix 时间戳。
| 调用 | 返回 |
|---|---|
$request->startTime() |
整数秒:1711234567 |
$request->startTime(true) |
带亚秒精度的浮点数:1711234567.3412 |
<?php
$elapsed = microtime(true) - $request->startTime(true);
error_log(sprintf("Request took %.3fs so far", $elapsed));属性
$request->attributes(): AttributesInterface返回这个 Request 对象持有的可变属性容器。使用属性可以把数据从中间件交给路由处理器,以及任何接收到同一个对象的其他代码,而无需使用全局变量。
<?php
// In authentication middleware
$request->attributes()->set('user', $authenticatedUser);
// In the route handler
$user = $request->attributes()->get('user');oxphp_http_request() 每次调用都会返回一个新的 Request,你从中取得的容器也是一个新的空容器。通过一次调用写入的数据,无法通过另一次调用读到:get() 会返回默认值,而且不会有任何警告或日志。
<?php
oxphp_http_request()->attributes()->set('tenant_id', $id);
oxphp_http_request()->attributes()->get('tenant_id'); // null —— 第二个 Request,第二个容器如果像上面的示例对 $request 所做的那样把对象传递下去,所有持有它的代码读写的都是同一个容器,包括你把它传入的纤程(Fiber)。自行调用 oxphp_http_request() 的纤程会得到它自己的容器,与其他调用者一样。由于 PHP 纤程是协作式的,不会有两个纤程同时访问一个容器。
请求之间不需要任何重置。容器会随持有它的对象一起释放,下一个请求的处理器调用 oxphp_http_request() 时会得到它自己的容器。
会话
$request->session(): ?SessionInterface返回 $_SESSION 的只读视图。若尚未调用 session_start(),则返回 null。会话管理(启动、保存、销毁、写入值)使用标准的 PHP 会话函数。
<?php
session_start();
$session = $request->session();
$userId = $session->get('user_id');
$isAdmin = $session->get('is_admin', false);
// Write session data using standard PHP functions
$_SESSION['last_seen'] = time();SessionInterface
SessionInterface 是活动会话的只读视图。
namespace OxPHP\Http;
interface SessionInterface
{
public function id(): string;
public function name(): string;
public function get(string $key, mixed $default = null): mixed;
public function has(string $key): bool;
public function all(): array;
}| 方法 | 说明 |
|---|---|
id() |
会话 ID |
name() |
会话名称(默认:"PHPSESSID") |
get(key, default) |
单个会话值,若键不存在则为 $default |
has(key) |
若键存在于 $_SESSION 中则为 true |
all() |
以数组形式返回所有会话数据 |
会话值反映的是调用时 $_SESSION 的当前状态,而非首次调用 session() 时的状态。
UploadedFileInterface
UploadedFileInterface 表示单个上传文件。
namespace OxPHP\Http;
interface UploadedFileInterface
{
public function name(): string;
public function clientType(): string;
public function type(): string;
public function size(): int;
public function tmpPath(): string;
public function error(): int;
public function isValid(): bool;
public function moveTo(string $destination): bool;
}| 方法 | 说明 |
|---|---|
name() |
客户端发送的原始文件名 |
clientType() |
客户端声明的 MIME 类型——不要在安全决策中信任此值 |
type() |
使用魔数(magic byte)检测(mime_content_type(),来自 ext/fileinfo),根据文件的实际内容确定的 MIME 类型。当无法确定类型时返回 "application/octet-stream"。在首次调用时检测,并保存在该对象上(参见类型检测)。 |
size() |
文件大小(字节) |
tmpPath() |
磁盘上临时文件的路径 |
error() |
UPLOAD_ERR_* 常量之一 |
isValid() |
当 error() 为 UPLOAD_ERR_OK 时返回 true |
moveTo(path) |
使用 move_uploaded_file() 将文件移动到 $path,不检测其类型。若文件无效或移动失败则返回 false。移动之后 type() 读取的是什么,参见类型检测。 |
在使用上传文件之前,务必先检查 isValid()。在做安全敏感的决策时,应使用 type() 而非 clientType():
<?php
$file = $request->file('document');
if (!$file || !$file->isValid()) {
http_response_code(400);
echo json_encode(['error' => 'Upload failed or missing']);
return;
}
$detectedMime = $file->type();
$allowed = ['application/pdf', 'image/jpeg', 'image/png'];
if (!in_array($detectedMime, $allowed, true)) {
http_response_code(415);
echo json_encode(['error' => "File type not allowed: $detectedMime"]);
return;
}
$file->moveTo('/var/uploads/' . bin2hex(random_bytes(8)) . '.pdf');类型检测
第一次调用 type() 会读取文件,并把结果保存在对象上;之后在该对象上的调用直接返回这个结果,不会再次读取文件。这也包括检测失败后得到的 "application/octet-stream",即使第一次调用是在会把警告转换为异常的错误处理器下抛出了异常也是如此:之后的调用会直接返回它,不再产生警告。来自另一次 file() 或 files() 调用的对象,会在它自己的第一次调用时重新读取文件。
如果没有 mime_content_type()(未加载 ext/fileinfo,或该函数被列在 disable_functions 中),每次本应读取文件的调用都会产生一个警告,并返回 "application/octet-stream" 而不保存它;在会把警告转换为异常的错误处理器下则会抛出异常。OxPHP 还会以 warn 级别为每个进程记录一行相关日志,写入服务器日志,或者在设置了 error_log 时写到它指向的位置。这些警告本身则像其他任何 PHP 警告一样被记录。
moveTo() 不检测类型。如果在移动之前没有在该对象上调用过 type(),那么移动成功后它的第一次调用会读取目标位置的文件:
- 本地路径:读取的是被移动的文件,通过移动操作放置它时所用的绝对路径找到。之后更改工作目录不会产生影响,包括 PHP 在运行 shutdown 函数和析构函数之前自行做的那次更改。
- 流包装器 URL:会通过其包装器重新打开,并根据返回的内容检测类型。如果包装器无法以读取方式打开该 URL(会产生警告),或无法对它执行
stat(例如compress.zlib://;在 PHP 8.4 上会产生警告),type()会返回"application/octet-stream",在会把警告转换为异常的错误处理器下则会抛出异常。 data:URL:永远不会被解码。move_uploaded_file()会把它当作本地文件名重命名过去,type()读取的就是那个文件;如果重命名失败,则会复制到该 URL,而该 URL 不接受写入:只有空的上传能通过,且什么也不会写入,type()找不到任何文件。- 同一上传的另一个对象:如果在移动之前没有调用过它的
type()(例如通过file()再次获取的对象),它找不到文件。mime_content_type()会因文件缺失产生一个警告,该对象的type()会返回"application/octet-stream",在会把警告转换为异常的错误处理器下则会抛出异常。
当目标是流包装器,或者被移动的文件在你查询其类型之前可能会被再次移动、删除或重写时,请在 moveTo() 之前调用 type()。该对象会保存那次调用检测到的结果。
AttributesInterface
AttributesInterface 是请求对象中唯一可变的部分。它用于存放由你自己的代码推导出的元数据(已认证用户、解析后的路由参数、区域设置、特性开关),并在同一调用链的后续环节再次读取。只有接收到持有它的 Request 对象的代码才能看到这些数据;参见 属性。
namespace OxPHP\Http;
interface AttributesInterface
{
public function get(string $key, mixed $default = null): mixed;
public function set(string $key, mixed $value): void;
public function has(string $key): bool;
public function remove(string $key): void;
public function all(): array;
}| 方法 | 说明 |
|---|---|
get(key, default) |
返回 $key 对应的值,若不存在则返回 $default |
set(key, value) |
存储一个值 |
has(key) |
若该键已被设置则为 true |
remove(key) |
移除该键 |
all() |
以关联数组形式返回所有属性 |
异常
在活动请求上下文之外调用 oxphp_http_request() 会抛出 OxPHP\Http\Exception 命名空间中的异常。
namespace OxPHP\Http\Exception;
class NoActiveRequestException extends \RuntimeException {}
class AsyncContextException extends NoActiveRequestException {}
class WorkerIdleException extends NoActiveRequestException {}| 异常 | 抛出时机 |
|---|---|
NoActiveRequestException |
没有活动的 HTTP 请求:CLI、MINIT、shutdown 之后,或生命周期超过其请求的纤程 |
AsyncContextException |
在 oxphp_async() 回调内部——异步工作进程运行在没有请求上下文的独立线程上 |
WorkerIdleException |
工作进程模式下,两次请求之间——工作进程正在等待下一个请求 |
AsyncContextException 和 WorkerIdleException 都继承自 NoActiveRequestException,因此捕获基类即可处理所有情况。
<?php
try {
$request = oxphp_http_request();
} catch (\OxPHP\Http\Exception\AsyncContextException $e) {
// Inside oxphp_async() — no request context here
} catch (\OxPHP\Http\Exception\WorkerIdleException $e) {
// Worker is between requests — do not call oxphp_http_request() here
} catch (\OxPHP\Http\Exception\NoActiveRequestException $e) {
// Any other case with no active request
}在正常的请求处理代码中,你不需要这个 try/catch。这种异常防护在可能于请求上下文之外运行的引导(bootstrap)代码中才有用。
SUPERGLOBALS_ENABLED
SUPERGLOBALS_ENABLED=true # default — full backward compatibility
SUPERGLOBALS_ENABLED=false # 不构建 $_SERVER 的请求键和 $_GET默认情况下,OxPHP 会像往常一样填充 $_GET、$_POST、$_COOKIE、$_FILES 和 $_SERVER。在此模式下,HTTP 对象 API 与超全局变量并存可用。
设置 SUPERGLOBALS_ENABLED=false 会跳过向 PHP 描述请求的工作:不注册 CGI 变量和 HTTP_* 变量,也不把查询字符串交给 PHP 解析。其效果比名字所暗示的要窄。请求体和 Cookie 请求头无论如何都会到达 PHP,而 PHP 会照常构建由它们派生的数组,与此设置无关:
| 功能 | SUPERGLOBALS_ENABLED=false 时的行为 |
|---|---|
oxphp_http_request() |
可用;所有方法都能正常返回,包括 query() 和 payload() |
php://input |
可用(它是一个流,不是超全局变量) |
$_SESSION |
可用(由 PHP 的会话模块管理) |
header()、headers_list() |
可用(SAPI 输出函数) |
session_start()、session_*() |
可用(原生 PHP 函数) |
$_POST、$_FILES、$_COOKIE |
照常填充。 它们由请求体和 Cookie 请求头构建,而此设置不会影响这两者 |
$_REQUEST |
填充。PHP 照常合并它(参见 超全局变量);只缺少 $_GET 本应贡献的部分 |
$_GET |
空 |
$_SERVER |
请求期间只有四个键:REQUEST_TIME、REQUEST_TIME_FLOAT、argc 和 argv,全部由 PHP 自己注册,而非由服务器注册。REQUEST_METHOD、REQUEST_URI、HTTP_* 键以及进程环境变量都不存在。工作进程模式的引导过程不同: oxphp_worker() 之上的代码在任何请求之前运行,它看到的 $_SERVER 按设置开启时的方式构建,包含全部进程环境变量,外加一个占位的 REQUEST_URI,值为 / |
因此,此设置并不能让请求数据远离 PHP 的全局变量,也不会阻止表单请求体被解析。它只跳过每个请求的 $_SERVER 填充(CGI 变量、HTTP_* 变量以及进程环境变量),以及 PHP 把查询字符串解析到 $_GET 的过程。
基于 $_SERVER['REQUEST_URI'] 做路由的代码在此设置下在那里什么也找不到,必须改为读取 oxphp_http_request()->path()。在工作进程入口脚本中,这适用于处理器。处理器之上的引导代码看到的是表中所述的占位值 /,那同样不是真实的路由。
使用 oxphp_superglobals_enabled() 在运行时检查当前设置:
<?php
if (!oxphp_superglobals_enabled()) {
$method = oxphp_http_request()->method();
} else {
$method = $_SERVER['REQUEST_METHOD'];
}工作进程模式
在工作进程模式下,oxphp_http_request() 每次调用都会构建一个新的 Request,而不是每个请求只构建一次。请在处理器内部调用它,并把对象传递到需要的地方,就像下面的示例那样。
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
$app = new MyApp\Application();
oxphp_worker(function () use ($app) {
$request = oxphp_http_request();
$app->handle($request);
});不要让对象的存活时间超过构建它的处理器。跨请求持有的对象不会失效,也永远不会被报告为过期。method()、path()、header() 以及其他不带缓存的读取方法,返回的是你调用时工作线程正在处理的那个请求的数据。带缓存的值(query() 数组、解析后的 payload()、属性容器)保存的是各个读取方法在首次调用时捕获的内容;在当前请求期间首次调用的读取方法捕获的是当前请求的数据。下一个请求开始时,Request 上的任何东西都不会被清除。请求之间的软重置实际涵盖哪些内容,列在 哪些会重置,哪些会持续存在 中。
IDE 支持
安装 stub 包,即可在 PhpStorm、VS Code 或任何支持 LSP 的编辑器中获得自动补全和类型检查:
composer require --dev oxphp/stubsstubs 包提供了:
oxphp-stubs/
├── OxPHP/Http/
│ ├── RequestInterface.php
│ ├── SessionInterface.php
│ ├── UploadedFileInterface.php
│ ├── AttributesInterface.php
│ ├── Request.php
│ ├── Session.php
│ ├── UploadedFile.php
│ ├── Attributes.php
│ └── Exception/
│ ├── NoActiveRequestException.php
│ ├── AsyncContextException.php
│ └── WorkerIdleException.php
└── functions.php不会添加任何运行时依赖。该包仅用于 require-dev。
示例
传统模式
<?php
$request = oxphp_http_request();
$method = $request->method(); // "GET"
$path = $request->path(); // "/api/articles"
$page = $request->query('page', 1); // "2" or 1 (default)
// Authorization header
if (!$request->hasHeader('Authorization')) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$token = $request->header('Authorization');
// Structured logging with request metadata
error_log(sprintf(
'[%s] %s %s from %s',
oxphp_request_id(),
$method,
$path,
$request->ip()
));带 JSON 请求体的 POST
<?php
$request = oxphp_http_request();
if (!$request->isMethod('POST')) {
http_response_code(405);
exit;
}
$email = $request->payload('email');
$password = $request->payload('password');
if (!$email || !$password) {
http_response_code(400);
echo json_encode(['error' => 'email and password are required']);
exit;
}
// payload() handles JSON, form-urlencoded, and multipart
// No manual json_decode() or $_POST check needed
$user = authenticate($email, $password);
header('Content-Type: application/json');
echo json_encode(['token' => $user->generateToken()]);中间件属性
<?php
// auth-middleware.php
function authenticate_request(\OxPHP\Http\RequestInterface $request): void
{
$token = $request->header('Authorization');
if (!$token) {
http_response_code(401);
exit;
}
$user = verify_token(str_replace('Bearer ', '', $token));
if (!$user) {
http_response_code(403);
exit;
}
$request->attributes()->set('user', $user);
}
// route-handler.php
$request = oxphp_http_request();
authenticate_request($request);
$user = $request->attributes()->get('user');
echo json_encode(['id' => $user->id, 'name' => $user->name]);带会话的工作进程模式
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
oxphp_worker(function () {
$request = oxphp_http_request();
if ($request->path() === '/login' && $request->isMethod('POST')) {
$username = $request->payload('username');
$password = $request->payload('password');
if (verify_credentials($username, $password)) {
session_start();
$_SESSION['user'] = $username;
$_SESSION['authenticated'] = true;
session_write_close();
header('Location: /dashboard');
} else {
http_response_code(401);
echo 'Invalid credentials';
}
return;
}
if ($request->path() === '/dashboard') {
session_start();
$session = $request->session();
if (!$session || !$session->get('authenticated')) {
session_write_close();
header('Location: /login');
return;
}
echo 'Welcome, ' . htmlspecialchars($session->get('user'));
session_write_close();
}
});在每条退出路径上调用 session_write_close(),可以让写入以及对保存处理器所锁定的一切的释放,发生在应用选择的时间点。未关闭的会话仍会在其请求结束时被写入并关闭,前提是工作进程正在处理的其他请求都不在该会话中。若有其他请求在该会话中,它就会保持打开,保存处理器也仍持有它锁定的所有东西,直到工作进程将其交还。一个请求如果打开了某个会话,或在该会话打开期间被接纳,就算在该会话中,无论它是否调用过 session_start()。因此,在这个时间窗口内被接纳的长时间运行的流,会让会话在流运行期间一直保持打开,而期间被接纳的请求,即使在打开该会话的请求已经返回之后,也仍会被交给这个会话。
无论是自己关闭会话,还是请求结束时的关闭,都无法隔离相互重叠的请求:工作进程在正在运行的请求挂起时就会接纳新请求,而在挂起点仍处于打开状态的会话,会被交给在该时间窗口内被接纳的请求。在该挂起点之前用 session_write_close() 关闭的会话会保留其 id,供在该窗口内被接纳的请求通过自己的 session_start() 沿用。参见 工作进程模式。
给你的智能体的提示词
这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。
把我的代码从超全局变量迁到 OxPHP 的请求 API 上。通过 OxPHP 文档 MCP 服务器读取 `php/request-api` 页面。
找出这个仓库里直接读取请求数据的地方,并把其中那些改写之后会改变行为、而不只是改变写法的地方改给我看。