Skip to content

请求解析 request_parse_body(8.4+)

PHP 8.4 新增了 request_parse_body() 函数,提供了统一、安全的 HTTP 请求体解析方式。该函数可以处理 application/x-www-form-urlencodedmultipart/form-dataapplication/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 原生模板

参考链接