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 端的数据结构,只返回那一个值。而像 $request->headers() 这样返回整个数组的调用则会构建一次数组,并在整个请求期间将其缓存在 PHP 对象中。
为什么用它而不是超全局变量?
- 内置 JSON 请求体解析。
$request->payload()无需额外代码即可解析application/json、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)会被解析为嵌套数组:
// Request: GET /search?q=oxphp&tags[]=php&tags[]=async
$q = $request->query('q'); // "oxphp"
$tags = $request->query('tags'); // ["php", "async"]
$all = $request->query(); // ["q" => "oxphp", "tags" => ["php", "async"]]找到的值始终是字符串。当键不存在时,$default 会原样返回。
解析后的请求体
$request->payload(?string $key = null, mixed $default = null): mixed返回解析后的请求体。请求体会根据 Content-Type 请求头进行解析:
| Content-Type | 返回 |
|---|---|
application/x-www-form-urlencoded |
字段值组成的关联数组 |
multipart/form-data |
文本字段值组成的关联数组 |
application/json |
解码后的数组或标量;无效 JSON 返回 null |
| 其他任何值 | null |
payload() 不限于 POST 请求。它适用于 PUT、PATCH 以及任何其他发送请求体的方法。解析结果会在首次调用时缓存,并在请求的整个生命周期内复用。
| 调用 | 返回 |
|---|---|
$request->payload() |
整个解析后的请求体 |
$request->payload('email') |
单个字段的值,若不存在则为 null |
$request->payload('email', '') |
单个字段的值,若不存在则为 '' |
<?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。
$request->cookies(): array以名值对的关联数组形式返回所有 cookie。
<?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[]") |
<?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 中最右侧的不可信地址。否则它是直接对端的 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返回当前请求的可变属性容器。使用属性可以在中间件、路由处理器以及同一请求中的其他代码之间共享数据,而无需使用全局变量。
<?php
// In authentication middleware
$request->attributes()->set('user', $authenticatedUser);
// In the route handler
$user = $request->attributes()->get('user');属性是按请求隔离的,在工作进程模式下每个新请求都会重置。使用纤程(Fiber)时,属性会在同一工作线程上为同一请求运行的所有纤程之间共享。由于 PHP 纤程是协作式的,因此不可能发生并发访问。
会话
$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 类型。当无法确定类型时返回 "application/octet-stream"。首次调用时缓存。 |
size() |
文件大小(字节) |
tmpPath() |
磁盘上临时文件的路径 |
error() |
UPLOAD_ERR_* 常量之一 |
isValid() |
当 error() 为 UPLOAD_ERR_OK 时返回 true |
moveTo(path) |
将文件移动到 $path。移动前会调用 type()。若文件无效或移动失败则返回 false。 |
在使用上传文件之前,务必先检查 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');AttributesInterface
AttributesInterface 是请求对象中唯一可变的部分。它用于存储按请求隔离的元数据——已认证用户、解析后的路由参数、区域设置、特性开关——这些数据需要被应用的多个部分访问。
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 # superglobals are empty arrays默认情况下,OxPHP 会像往常一样填充 $_GET、$_POST、$_COOKIE、$_FILES 和 $_SERVER。在此模式下,HTTP 对象 API 与超全局变量并存可用。
设置 SUPERGLOBALS_ENABLED=false 会使这些数组为空,从而消除为每个请求构建它们的开销。无论此设置如何,以下功能仍然有效:
| 功能 | SUPERGLOBALS_ENABLED=false 时的行为 |
|---|---|
oxphp_http_request() |
始终可用 |
php://input |
可用(它是一个流,不是超全局变量) |
$_SESSION |
可用(由 PHP 的会话模块管理) |
header()、headers_list() |
可用(SAPI 输出函数) |
session_start()、session_*() |
可用(原生 PHP 函数) |
$_GET、$_POST、$_COOKIE、$_FILES、$_SERVER |
空数组 |
使用 oxphp_superglobals_enabled() 在运行时检查当前设置:
<?php
if (!oxphp_superglobals_enabled()) {
$method = oxphp_http_request()->method();
} else {
$method = $_SERVER['REQUEST_METHOD'];
}工作进程模式
在工作进程模式下,每个到来的请求都会创建一个新的 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);
});当下一个请求开始时,Request 对象上的所有缓存(已解析的请求头、cookie、查询参数、负载)都会自动清除。
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;
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')) {
header('Location: /login');
return;
}
echo 'Welcome, ' . htmlspecialchars($session->get('user'));
}
});