编程

PHP 8.4: 新增 request_parse_body 函数

193 2024-03-13 16:47:00

PHP自动解析 HTTP POST 请求,以填充 $_POST$_FILES 超级全局变量。然而,使用 PUTPATCH 等方法的其他 HTTP 请求不会自动解析,而是由 PHP 应用来解析请求数据。

随着越来越多地使用 HTTP 方法(如 PUTDELETEPATCH)的 REST API 的流行,一致地解析 HTTP 请求数据非常重要。然而,对于现有的 PHP 程序来说,开始自动解析非 POST 请求的 HTTP 请求数据可能是一个破坏性的变化。

PHP 在 php://input 中提供了一个流式包装器,其包含了请求数据。对于使用的 enctype="multipart/form-data"POST 请求,流式包装器保留为空,因为它自动解析并消费以填充 $_POST$_FILES 变量。自动 $_POST/$_FILES 处理可以使用 enable_post_data_reading=Off INI 设置进行控制。

enable_post_data_reading INI 值设置为 Off,或者当 HTTP 请求方法是 POST 之外的值时, php://input 流式包装器可以在用户空间 PHP 代码中读取,以解析 HTTP 请求数据。

curl --request PUT \
     --location 'https://example.com/post.php' \
     --form 'test="123"'

对于 PHP 应用,以上 Curl 调用的表单数据可以从 php://input 流中读取。

echo file_get_contents('php://input');
----------------------------690112416382325217174003
Content-Disposition: form-data; name="test"
123
----------------------------690112416382325217174003--

新增 request_parse_body 函数

PHP 8.4 添加了一个名为 request_parse_body 的新函数,它为其他 HTTP 请求方法暴露了 PHP 内置的请求解析功能。

/**  
 * Parse and consume php://input and return the values for $_POST
 * and $_FILES variables.
 * 
 * @param array<string, int|string>|null $options Overrides for INI values
 * @return array<int, array> Array with key 0 being the post data  
 *   (similar to $_POST), and key 1 being the files ($_FILES).  
 */
function request_parse_body(?array $options = null): array {}

当调用时,request_parse_body 函数读取在 php://input 流上的整个内容并创建可在 $_POST$_FILES 变量中使用的值。

返回值是一个包含两个键(01)的数组,包含的解析值可用作 $_POST(数组键索引 0)和 $FILES (索引 1)。这两个数组键会一直存在,即使没有请求数据,和/或文件。

可以从其返回值中直接填充 $_POST$_FILES 值:

[$_POST, $_FILES] = request_parse_body();

请注意,请求解析继续绑定到 INI 指令设置的限制。例如,如果 post_max_size 指令(限制请求的最大大小)设置为 2000 字节,则试图用大于该指令的请求调用 request_parse_body 函数会继续导致错误。

通过传递 $options 参数,可以用更小或更大的值覆盖请求解析选项。

如果试图解析的请求违反了 INI 指令或自定义选项中设置的限制,那么 request_parse_body 函数将抛出一个名为RequestParseBodyException 的新异常。

重写请求解析选项

$options 参数可用于传递与请求解析相关的 INI 值数组。这些值不需要小于全局配置。这有利于选择性地处理比 INI 文件中设置的限制更小或更大的限制。

例如,要解析对 post_max_size INI 指令具有更高或更小限制的请求,请使用所需的新值调用 request_parse_body 函数:

request_parse_body(['post_max_size' => 1024]);

$options 数组只接受以下重写: 

INI/$option描述
post_max_sizePHP 接受的最大 POST 数据大小。其值可为 0 以禁用限制。
max_input_vars可以接受多少个 GET/POST/COOKIE 输入变量
max_multipart_body_parts可以接受多少个 multipart body parts (包括输入变量和上传文件s) 。
max_file_uploads单个请求可以上传的最大文件数量
upload_max_filesize允许的上传文件的最大大小限制

这些键的值必须是整数或数量字符串(例如 ini_parse_quantity 函数允许的值)。

ValueError 异常中,在上面列出的列表之外传递INI 指令:

request_parse_body(['arbitrary_value' => 42]);
ValueError: Invalid key 'arbitrary_value' in $options argument

传递非整数或者非数量字符串值会导致 PHP 警告:

request_parse_body(['post_max_size' => 'arbitrary_value']);
Warning: Invalid quantity "arbitrary_value": no valid leading digits, interpreting as "0" for backwards compatibility

传递非字符串和非整数值作为 $options 键的值将会导致 ValueError 异常:

request_parse_body(['post_max_size' => []]);
ValueError: Invalid array value in $options argument

RequestParseBodyException 异常类

RequestParseBodyException 是一个在全局命名空间中声明的异常类,它扩展了 Exception 类。

class RequestParseBodyException extends Exception {}

如果 request_parse_body 函数不能解析请求数据,将会抛出 RequestParseBodyException 异常。如果提供的请求数据无效,没有发送 Content-Type header,或请求数据超出 INI 指令及 $options 参数的约束设置时,也会抛出此异常。

以下是 RequestParseBodyException 异常的列表和它们的起因:

RequestParseBodyException: Request does not provide a content type
请求不包含 Content-Type header。

RequestParseBodyException: Content-Type ARBITRARY_TYPE is not supported
Content-Type header 包含的值不是 multipart/form-dataapplication/x-www-form-urlencoded

RequestParseBodyException: Missing boundary in multipart/form-data POST data
请求不包含边界(boundary)。确保请求的正确格式化为 multipart/form-data 或者 application/x-www-form-urlencoded

RequestParseBodyException: POST Content-Length of ... bytes exceeds the limit of ... bytes
内容长度超过  ($options parameter) 或者 INI 指令中设置的 post_max_size 值。

RequestParseBodyException: Multipart body parts limit exceeded ... To increase the limit change max_multipart_body_parts in php.ini
请求数据部分超过  ($options parameter) 或者 INI 指令中设置的 max_multipart_body_parts 值。

RequestParseBodyException: Input variables exceeded ... To increase the limit change max_input_vars in php.ini.
请求数据部分超过  ($options parameter) 或者 INI 指令中设置的 max_input_vars值。

RequestParseBodyException: Maximum number of allowable file uploads has been exceeded
上传的文件数量超过  ($options parameter) 或者 INI 指令中设置的 max_file_uploads

request_parse_body 警告

request_parse_body 被设计成每个请求只调用一次。它没有提供指定要解析的字符串的方法,并且会破坏性地消耗 php://input。在随后的调用中,该函数将返回一个包含空数据的数组,并且 php://input 流在第一次 request_parse_body() 调用之后,将为空。

request_parse_body 的非幂等行为
请注意,调用 request_parse_body 函数具有潜在的破坏性行为,包括消耗 php://input 流并清空其内容。

  • 调用 request_parse_body 函数消费 php://input 流。php://input 流将为空
  • 如果之前读取过 php://input 流(e.g. file_get_contents('php://input')request_parse_body 函数将返回空结果(i.e. [0 => [], 1 => []]).
  • request_parse_body 函数不直接修改 $_POST$_FILES 全局变量;如果有需要由 PHP 应用来重写这些变量。g
  • 只有第一次调用 request_parse_body 函数时返回解析数据。随后的调用将返回空结果 (i.e. [0 => [], 1 => []]).
  • 即使该函数抛出异常,仍然会消费 php://input 并清空,随后的 request_parse_body 调用将返回空结果。

向后兼容性影响

此函数不能进行 polyfill,因为它需要利用对基础服务器 API(SAPI)的调用来获取原始数据。

除非 PHP 应用程序声明了自己的 request_parse_body 函数,否则此更改不应导致任何向后兼容性问题。