☰
PSR-7 头部与查询串辅助方法全解析:Header、Query 与 ASCII 无区域大小写工具
2026/9/27 7:17:05 网站建设 项目流程
  • 后端

【免费下载链接】psr7

PSR-7 HTTP message library

项目地址:https://gitcode.com/gh_mirrors/ps/psr7
点击查看免费下载

本篇技术指南聚焦 guzzlehttp/psr7 仓库中 docs/header-and-query-helpers.md 所讲解的辅助工具层:如何解析结构化头部参数、按逗号拆分列表型头部、解析与构建查询字符串,以及一批与区域设置无关的 ASCII 大小写转换与无大小写比较工具。读完本文,你将掌握Header、Query与Utils三个静态工具类的核心 API 语义、底层实现细节与边界行为,并能在自己的 HTTP 客户端、服务端中间件或协议解析代码中直接复用这些能力。

辅助方法在 PSR-7 消息模型中的定位

PSR-7 规范把 HTTP 消息抽象为MessageInterface(请求与响应的公共部分),提供getHeader()、getHeaderLine()、withHeader()等基本头部操作;但规范刻意不规定“如何解析头部内部的结构”。guzzlehttp/psr7 通过独立的工具类补齐了这一层:Header负责把头部值拆成可编程处理的结构化数据,Query负责查询字符串的解析与构建,Utils提供协议级字符串处理原语。关于消息层的基础行为,可参考 PSR-7 Messages,本页内容则是对它的补充。

从源码结构看,这三个类全部位于 src/Header.php、src/Query.php、src/Utils.php,且都声明为final class并私有化构造函数——即只提供静态方法、不允许被实例化或继承,属于典型的“工具类”设计。

结构化头部参数解析:Header::parse

签名

public static function parse(string|array $header): array

作用:把分号分隔的头部参数解析为关联数组,每个逗号分隔的头部值对应一个数组;没有值的参数按整数下标追加为值。

parse()的内部流程(见 src/Header.php)分三步:

  1. 先把输入归一化为数组((array) $header),逐值处理;
  2. 对每个值调用splitList()按逗号拆出独立的“条目”(例如Link头中的每个<uri>; rel=...);
  3. 对每个条目调用私有方法splitParameters()按分号切分参数,再通过正则/<[^>]+>|[^=]+/将每个key=value拆成键值对——命中<...>形式(如Link头中的角括号 URI)或没有=的参数时,整体作为整数下标的值保存。

splitParameters()(src/Header.php)实现了一个带引号感知的小型词法扫描器:它会跟踪"引号状态与\转义状态,只在引号外遇到;才切分。因此foo="a;b"中的分号不会把值切断,测试用例 tests/HeaderTest.php 中的'foo="a;b"; bar=baz'、'foo="a;b\"c"; bar=baz'都验证了这一点。

典型用例——解析Link响应头:

use GuzzleHttp\Psr7\Header; $link = '<http:/.../front.jpeg>; rel="front"; type="image/jpeg", ' . '<http://.../back.jpeg>; rel=back; type="image/jpeg"'; $parsed = Header::parse($link); // [ // ['<http:/.../front.jpeg>', 'rel' => 'front', 'type' => 'image/jpeg'], // ['<http://.../back.jpeg>', 'rel' => 'back', 'type' => 'image/jpeg'], // ]

当 PCRE 内部出错(例如测试中人为把pcre.backtrack_limit压到 1)时,parse()会抛出带preg_last_error_msg()说明的\RuntimeException,见 tests/HeaderTest.php。

列表型头部拆分:Header::splitList

签名

public static function splitList(string|string[] $header): string[]

作用:把被定义为“逗号分隔列表”的 HTTP 头部拆成单个值,并移除空值。

典型用例:

$knownEtags = Header::splitList($request->getHeader('if-none-match'));

getHeader()返回的可能是字符串数组(PSR-7 允许多行同名单个头部值),splitList()会先归一化数组再逐值扫描。扫描器同样是引号与转义感知的:只有在引号外的逗号才作为分隔符,从而正确处理"foo,bar"这类 ETag 内部含逗号的情况。normalizeProvider数据集中覆盖了 tests/HeaderTest.php 的'"foo", "foo,bar", "bar"'以及 tests/HeaderTest.php 中Cache-Control带引号逗号、转义空格、转义引号等场景。

适用与禁用边界(务必遵守):

  • 适用:accept、cache-control、if-none-match等按 RFC 定义为列表的头部;
  • 禁用:user-agent、set-cookie等不是列表语义的头部。它们的值本身可能含逗号,拆分会破坏原始数据。

另外,传入非字符串值(如整数、对象)会抛出带说明文字的\TypeError(见 src/Header.php 与测试 tests/HeaderTest.php)。

查询字符串解析:Query::parse

签名

public static function parse(string $str, int|bool $urlEncoding = true): array

作用:把查询字符串解析为关联数组。同一键出现多次时,值自动合并为数组;不会把 PHP 风格的嵌套数组(foo[a]=1&foo[b]=2)解析成嵌套关联数组,而是保留字面键名:

use GuzzleHttp\Psr7\Query; Query::parse('foo[a]=1&foo[b]=2'); // ['foo[a]' => '1', 'foo[b]' => '2']

urlEncoding参数决定解码策略(src/Query.php):

取值解码行为
true(默认)rawurldecode(),并把+替换为空格(模拟parse_str()风格)
PHP_QUERY_RFC3986仅rawurldecode(),+保持字面加号
PHP_QUERY_RFC1738urldecode(),+解码为空格
false完全不解码,键值按原始文本保留

测试 tests/QueryTest.php 演示了var=foo+bar在 RFC3986 下得到'foo+bar'、在 RFC1738 下得到'foo bar'的差异。

边界行为(均可从 tests/QueryTest.php 的数据集验证):

  • 空字符串返回[];
  • 没有=的键值为null(如'foo' => null);
  • 带=的值为空字符串('0=' => '');
  • 保留q.a这类点号键,不像 PHPparse_str()会把.转成_;
  • 不截断尾部等号:'data=abc='得到'abc=';
  • 重复键合并为数组,且保留null与空串等特殊值('q&q=&q=a'得到['q' => [null, '', 'a']])。

查询字符串构建:Query::build

签名

public static function build(array $params, int|false $encoding = PHP_QUERY_RFC3986, bool $treatBoolsAsInts = true): string

作用:把键值对数组构建成查询字符串。build()可以直接消费parse()的返回值完成“解析→重建”往返(测试 tests/QueryTest.php 验证了parse($input, false)后再build(..., false)能还原原串)。

与http_build_query()的关键差异:遇到值为数组的键时,build()不会改写键名(http_build_query()会为嵌套数组生成foo[0]之类的带下标键),而是原样保留键并为数组中的每个元素生成一个key=value。例如['foo' => ['a', 'b']]生成foo=a&foo=b。

encoding参数(src/Query.php)控制编码器:

取值编码行为
PHP_QUERY_RFC3986(默认)rawurlencode()(空格 →%20)
PHP_QUERY_RFC1738urlencode()(空格 →+)
false不编码,原样输出
其他抛出\InvalidArgumentException('Invalid type')

treatBoolsAsInts控制布尔值序列化:为true时编码为0/1(与http_build_query()默认一致),为false时编码为false/true。测试见 tests/QueryTest.php。

值类型与异常(由私有方法normalizeValue()保证,src/Query.php):

  • null:只输出键,不输出=与值(['foo' => null]→foo);
  • 标量、实现__toString()的对象:转字符串后编码;
  • 嵌套数组、stdClass等不支持的类型:抛出InvalidArgumentException('Query string values must be scalar, null, or stringable objects');
  • 非有限浮点数(NAN、INF、-INF):抛出InvalidArgumentException('Query string values must be finite; non-finite floats are not supported.')。相关测试见 tests/QueryTest.php。

区域无关的 ASCII 大小写原语:Utils::asciiToLower/asciiToUpper/asciiUcFirst

签名

public static function asciiToLower(string $string): string public static function asciiToUpper(string $string): string public static function asciiUcFirst(string $string): string

作用与设计动机:HTTP 协议元素(头部字段名、方案名、主机名等)按规范只对 ASCII 字母做大小写折叠。PHP 原生的strtolower()/strtoupper()/ucfirst()在 PHP 8.2 之前会遵循LC_CTYPE区域设置——这意味着在不同服务器区域下,同一个字符串可能被转换成不同结果,甚至影响非 ASCII 字节。这三个方法用strtr()做纯 ASCII 字母表映射(src/Utils.php),完全不受区域设置影响,并且所有非 ASCII 字节原样保留。

行为验证(tests/UtilsTest.php):

Utils::asciiToLower('X-Checksum'); // 'x-checksum' Utils::asciiToUpper('x-Checksum'); // 'X-CHECKSUM' Utils::asciiUcFirst('index'); // 'Index' Utils::asciiUcFirst('0abc'); // '0abc'(首字符非字母,原样返回) Utils::asciiUcFirst(''); // ''(空串直接返回)

注意asciiUcFirst('x-a')返回'X-a'——它只作用于第一个字符,不会像ucwords()那样把-之后的字母也大写。

无大小写敏感比较:Utils::caselessContains/caselessEquals/caselessRemove

签名

public static function caselessContains(string $haystack, string $needle): bool public static function caselessEquals(string $left, string $right): bool public static function caselessRemove(array $keys, array $data): array

作用:基于上述 ASCII 小写原语实现协议级的无大小写比较与过滤,全部与区域设置无关。

  • caselessContains:先把haystack与needle转成 ASCII 小写再调用str_contains()(src/Utils.php)。测试覆盖'Connection TIMEOUT after'包含'timeout'为真、'Connection reset'不包含为假、空 needle 恒为真(tests/UtilsTest.php)。适合匹配错误信息中的关键词,例如判断传输层错误文本里是否出现timeout。
  • caselessEquals:两边转小写后比较(src/Utils.php)。caselessEquals('HOST', 'host')为真;非 ASCII 字符不会被“折叠”,caselessEquals("\xC4\xB0", 'i')(土耳其语点状大写 I)为假(tests/UtilsTest.php)。
  • caselessRemove:把$keys与$data的键都转小写后剔除匹配项,并保持原键名与顺序(src/Utils.php)。它被Utils::modifyRequest()内部用来按无大小写语义清理头部(src/Utils.php),例如remove_headers传['X-Checksum']也能删掉实际为x-checksum的头部。

实战组合:这几个方法配合使用即可实现“规范化头部是否存在/是否相等”的协议判断:

use GuzzleHttp\Psr7\Utils; $headers = ['Content-Type' => 'text/html', 'X-Checksum' => 'abc']; $remaining = Utils::caselessRemove(['x-checksum'], $headers); // ['Content-Type' => 'text/html'] $isKeepAlive = Utils::caselessEquals( $connectionHeader, 'keep-alive' );

相关阅读

  • PSR-7 Messages —— 消息层基础头部行为
  • Message Helpers —— 消息级辅助方法
  • URI Helpers —— URI 解析、规范化与比较
  • 本页涉及工具的完整源码:src/Header.php、src/Query.php、src/Utils.php;行为佐证测试:tests/HeaderTest.php、tests/QueryTest.php、tests/UtilsTest.php
  • 后端

【免费下载链接】psr7

PSR-7 HTTP message library

项目地址:https://gitcode.com/gh_mirrors/ps/psr7
点击查看免费下载

相关推荐

上一篇:如何用Micro-World创建沉浸式3D场景:从文本到交互式世界的完整教程
下一篇:VueDataV:企业级数据可视化大屏的3个关键突破与实施策略

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询