请求解析 request_parse_body(8.4+)
PHP 8.4 新增了 request_parse_body() 函数,提供了统一、安全的 HTTP 请求体解析方式。该函数可以处理 application/x-www-form-urlencoded、multipart/form-data 和 application/json 三种常见的内容类型,返回结构化的数据,是构建 API 接口的便捷工具。
前置知识
阅读本节前,建议先了解:GET 与 POST 请求、JSON 处理
基础概念
PHP 8.4 之前的请求体解析问题
php
<?php
// === PHP 8.4 之前的痛点 ===
// 1. POST form-urlencoded -> $_POST(自动解析)
// 2. POST multipart/form-data -> $_POST + $_FILES(自动解析)
// 3. POST JSON -> php://input(需手动 json_decode)
// 4. PUT/PATCH/DELETE form-urlencoded -> php://input(需手动 parse_str)
// 5. PUT/PATCH/DELETE JSON -> php://input(需手动 json_decode)
// 6. PUT/PATCH/DELETE multipart -> 需要第三方库或手动解析
// 不同方法和内容类型需要不同的处理方式
$method = $_SERVER['REQUEST_METHOD'];
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if ($method === 'POST' && str_contains($contentType, 'application/json')) {
$data = json_decode(file_get_contents('php://input'), true);
} elseif ($method === 'PUT' && str_contains($contentType, 'application/x-www-form-urlencoded')) {
parse_str(file_get_contents('php://input'), $data);
} elseif ($method === 'POST') {
$data = $_POST;
}
// ... 非常繁琐且容易出错request_parse_body() 的优势
php
<?php
// PHP 8.4+ 一行代码搞定
[$data, $files] = request_parse_body();
// 自动处理:
// - application/x-www-form-urlencoded
// - multipart/form-data
// - application/json
// - 任意 HTTP 方法(GET/POST/PUT/PATCH/DELETE)语法
函数签名
php
<?php
// request_parse_body(
// string $content_type = null,
// string $content_text = null,
// string $content_encoding = null
// ): array
//
// 返回: [array $parsed_body, array $uploaded_files]基本用法
php
<?php
declare(strict_types=1);
// === 方式一:自动从当前请求解析 ===
[$data, $files] = request_parse_body();
$data 是解析后的请求数据
$files 是上传文件数组(与 $_FILES 格式相同)
// === 方式二:解析自定义数据 ===
$jsonString = '{"name":"Alice","email":"alice@example.com"}';
[$data, $files] = request_parse_body('application/json', $jsonString);
$formString = 'username=Alice&email=alice%40example.com';
[$data, $files] = request_parse_body('application/x-www-form-urlencoded', $formString);
// === 方式三:指定编码 ===
[$data, $files] = request_parse_body(
content_type: 'application/x-www-form-urlencoded',
content_text: file_get_contents('php://input'),
content_encoding: 'gzip',
);详细说明
不同 Content-Type 的处理
php
<?php
declare(strict_types=1);
// === 1. application/x-www-form-urlencoded ===
// 请求体: username=alice&age=30&roles[]=admin&roles[]=user
[$data, $files] = request_parse_body();
// $data = ['username' => 'alice', 'age' => '30', 'roles' => ['admin', 'user']]
// $files = []
// 与传统方式对比
// $data = $_POST; // 效果相同,但 request_parse_body() 适用于所有 HTTP 方法
// === 2. multipart/form-data ===
// HTML 表单: <form method="POST" enctype="multipart/form-data">
[$data, $files] = request_parse_body();
// $data = ['username' => 'alice', 'email' => 'alice@example.com']
// $files = ['avatar' => ['name' => 'photo.jpg', 'type' => 'image/jpeg', ...]]
// 与传统方式对比
// $data = $_POST; $files = $_FILES;
// === 3. application/json ===
// 请求体: {"name":"Alice","age":30,"tags":["php","dev"]}
[$data, $files] = request_parse_body();
// $data = ['name' => 'Alice', 'age' => 30, 'tags' => ['php', 'dev']]
// $files = []
// 与传统方式对比
// $data = json_decode(file_get_contents('php://input'), true);
// === 4. 不支持的 Content-Type ===
// 请求体: <xml><name>Alice</name></xml> (Content-Type: application/xml)
[$data, $files] = request_parse_body();
// $data = null(无法解析)
// $files = null处理 PUT/PATCH/DELETE 请求体
php
<?php
declare(strict_types=1);
/**
* PHP 8.4+ 的 request_parse_body() 完美解决了
* 非 POST 方法的请求体解析问题
*/
$method = $_SERVER['REQUEST_METHOD'];
match ($method) {
'GET' => handleList(),
'POST' => handleCreate(),
'PUT' => handleReplace(),
'PATCH' => handleUpdate(),
'DELETE' => handleDelete(),
default => http_response_code(405),
};
function handleCreate(): void
{
[$data, $files] = request_parse_body();
// $data 是创建数据的关联数组
// $files 是上传的文件
// ...
}
function handleUpdate(): void
{
[$data, $files] = request_parse_body();
// PUT/PATCH 请求体现在可以像 POST 一样轻松解析
// 不再需要手动处理 php://input 和 parse_str
// ...
}
function handleDelete(): void
{
[$data, $files] = request_parse_body();
// 删除请求体中的额外数据
// ...
}实战示例:RESTful 控制器
php
<?php
declare(strict_types=1);
/**
* PHP 8.4+ RESTful API 控制器
* 使用 request_parse_body() 简化请求处理
*/
class UserController
{
public function __construct(
private readonly PDO $pdo,
) {
}
/**
* GET /api/users - 获取用户列表
*/
public function index(): never
{
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['default' => 1]]);
$perPage = filter_input(INPUT_GET, 'per_page', FILTER_VALIDATE_INT, ['options' => ['default' => 20]]);
$offset = ($page - 1) * $perPage;
$stmt = $this->pdo->prepare('SELECT id, username, email, created_at FROM users ORDER BY id DESC LIMIT ? OFFSET ?');
$stmt->execute([$perPage, $offset]);
$countStmt = $this->pdo->query('SELECT COUNT(*) FROM users');
$total = (int) $countStmt->fetchColumn();
ApiResponse::success([
'items' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'pagination' => [
'total' => $total,
'page' => $page,
'per_page' => $perPage,
],
]);
}
/**
* POST /api/users - 创建用户
*/
public function store(): never
{
[$data, $files] = request_parse_body();
// 验证
$errors = $this->validateCreate($data);
if (!empty($errors)) {
ApiResponse::error('Validation failed', 422, $errors);
}
// 创建用户
$hash = password_hash($data['password'], PASSWORD_DEFAULT);
$stmt = $this->pdo->prepare(
'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)'
);
$stmt->execute([$data['username'], $data['email'], $hash]);
$id = (int) $this->pdo->lastInsertId();
ApiResponse::success(['id' => $id], 'Created', 201);
}
/**
* PUT /api/users/{id} - 替换用户
*/
public function update(int $id): never
{
[$data, $files] = request_parse_body();
// 全量更新
$stmt = $this->pdo->prepare(
'UPDATE users SET username = ?, email = ? WHERE id = ?'
);
$stmt->execute([$data['username'], $data['email'], $id]);
ApiResponse::success(['updated' => $stmt->rowCount() > 0]);
}
/**
* PATCH /api/users/{id} - 部分更新用户
*/
public function patch(int $id): never
{
[$data, $files] = request_parse_body();
// 动态构建 UPDATE
$fields = [];
$values = [];
$allowedFields = ['username', 'email', 'status'];
foreach ($data as $key => $value) {
if (in_array($key, $allowedFields, true)) {
$fields[] = "{$key} = ?";
$values[] = $value;
}
}
if (empty($fields)) {
ApiResponse::error('No valid fields to update', 400);
}
$values[] = $id;
$sql = 'UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = ?';
$stmt = $this->pdo->prepare($sql);
$stmt->execute($values);
ApiResponse::success(['updated' => $stmt->rowCount() > 0]);
}
/**
* DELETE /api/users/{id} - 删除用户
*/
public function destroy(int $id): never
{
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) {
ApiResponse::error('User not found', 404);
}
ApiResponse::success(null, 'Deleted', 204);
}
private function validateCreate(array $data): array
{
$errors = [];
if (empty($data['username']) || mb_strlen($data['username']) < 3) {
$errors['username'] = 'Username must be at least 3 characters';
}
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Invalid email address';
}
if (empty($data['password']) || mb_strlen($data['password']) < 8) {
$errors['password'] = 'Password must be at least 8 characters';
}
return $errors;
}
}
// === 路由 ===
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$controller = new UserController($pdo);
// 匹配 /api/users 或 /api/users/{id}
if ($uri === '/api/users') {
match ($method) {
'GET' => $controller->index(),
'POST' => $controller->store(),
default => (function () { http_response_code(405); echo json_encode(['error' => 'Method Not Allowed']); })(),
};
} elseif (preg_match('#^/api/users/(\d+)$#', $uri, $matches)) {
$id = (int) $matches[1];
match ($method) {
'PUT' => $controller->update($id),
'PATCH' => $controller->patch($id),
'DELETE' => $controller->destroy($id),
default => (function () { http_response_code(405); echo json_encode(['error' => 'Method Not Allowed']); })(),
};
}兼容性处理
php
<?php
declare(strict_types=1);
/**
* 跨版本兼容的请求体解析
*/
function parseRequestBody(): array
{
if (PHP_VERSION_ID >= 80400 && function_exists('request_parse_body')) {
return request_parse_body();
}
// PHP 8.4 以下的兼容实现
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$rawBody = file_get_contents('php://input');
if (str_contains($contentType, 'application/json')) {
$data = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
return [$data ?? null, []];
}
if (str_contains($contentType, 'application/x-www-form-urlencoded')) {
parse_str($rawBody, $data);
return [$data, []];
}
if (str_contains($contentType, 'multipart/form-data')) {
return [$_POST, $_FILES];
}
return [null, []];
}
// 使用
[$data, $files] = parseRequestBody();与 $_POST / $_FILES 的关系
php
<?php
declare(strict_types=1);
// request_parse_body() 与传统超全局变量的关系:
//
// 1. 对于 POST application/x-www-form-urlencoded:
// - $_POST 已被 PHP 自动填充
// - request_parse_body() 也会返回相同的数据
// - 不需要调用两次
//
// 2. 对于 POST multipart/form-data:
// - $_POST 和 $_FILES 已被 PHP 自动填充
// - request_parse_body() 也会返回相同的数据
//
// 3. 对于 POST/PUT/PATCH application/json:
// - $_POST 为空
// - request_parse_body() 返回解码后的数组
//
// 4. 对于 PUT/PATCH application/x-www-form-urlencoded:
// - $_POST 为空
// - request_parse_body() 返回解析后的数组
// 重要:request_parse_body() 会消费 php://input 流
// 如果先调用了 file_get_contents('php://input'),再调用 request_parse_body()
// 可能得到空数据(取决于 SAPI 实现)注意事项
1. php://input 的消费
php
<?php
// request_parse_body() 会消费 php://input 流
// 先读取 php://input 再调用 request_parse_body() 可能导致问题
// 正确:直接使用 request_parse_body()
[$data, $files] = request_parse_body();
// 错误:先读取再解析
$rawBody = file_get_contents('php://input'); // 消费流
[$data, $files] = request_parse_body(); // 可能为空2. 大文件上传
php
<?php
// 对于大文件上传(multipart/form-data),
// request_parse_body() 的行为与 $_FILES 一致
// 受 php.ini 中 upload_max_filesize 和 post_max_size 限制
// 检查上传错误
[$data, $files] = request_parse_body();
if ($files === null) {
// 检查是否超过 post_max_size
if ($_SERVER['CONTENT_LENGTH'] > (int) ini_get('post_max_size') * 1024 * 1024) {
http_response_code(413);
echo json_encode(['error' => 'Request body too large']);
exit;
}
}最佳实践
1. 使用 request_parse_body() 统一请求处理
php
<?php
// PHP 8.4+ 项目中,统一使用 request_parse_body()
// 不再区分 POST/PUT/PATCH 的数据获取方式
// 不再手动处理 php://input + json_decode
// 不再手动处理 parse_str
// 旧方式
if ($method === 'POST') {
$data = $_POST;
} else {
$rawBody = file_get_contents('php://input');
if ($contentType === 'application/json') {
$data = json_decode($rawBody, true);
} else {
parse_str($rawBody, $data);
}
}
// 新方式(PHP 8.4+)
[$data, $files] = request_parse_body();下一节
继续学习:PHP 原生模板