- 电商
- 后端
【免费下载链接】opencart
A free shopping cart system. OpenCart is an open source PHP-based online e-commerce solution.
PSR-7 定义了 PHP 生态中 HTTP 消息(请求与响应)的统一接口契约,是 Guzzle、AWS SDK 等主流 HTTP 客户端实现互操作的基础。本文以 OpenCart 仓库内置的psr/http-message包(位于upload/system/storage/vendor/psr/http-message)为核心,完整讲解 HTTP 头的增删改查、消息体的读写与流指针操作,并结合仓库内的接口源码与 Guzzle 实现,帮助你写出符合标准、可复用的 HTTP 消息处理代码。读完本文,你将掌握 PSR-7 的不可变对象语义、Header 与 Stream 的全部操作手法,以及常见的指针陷阱与规避方案。
一、PSR-7 是什么:HTTP 消息的标准接口契约
PSR-7(PHP Standard Recommendation 7)定义了一套描述 HTTP 消息的接口。正如upload/system/storage/vendor/psr/http-message/docs/PSR7-Usage.md开头所述:所有符合 PSR-7 的应用程序都遵守这些接口,它们被创建出来是为了在中间件(middleware)实现之间建立统一标准。无论底层实现是哪个库,只要实现这些接口,行为就应该一致。
该包的核心继承关系如下(摘自原文档的说明):
RequestInterface、ServerRequestInterface、ResponseInterface都继承自MessageInterface,因为请求(Request)与响应(Response)本质上都是 HTTP 消息(HTTP Messages)。 使用ServerRequestInterface时,RequestInterface与Psr\Http\Message\MessageInterface的方法都被视为可用。
在 OpenCart 仓库中,该包以 Composer 依赖形式存在,完整文件结构为:
- 接口定义:
upload/system/storage/vendor/psr/http-message/src/(7 个接口文件) - 文档:
upload/system/storage/vendor/psr/http-message/docs/(PSR7-Interfaces.md方法速查、PSR7-Usage.md用法指南) - 包说明:
upload/system/storage/vendor/psr/http-message/README.md
需要特别强调的是(README 中的原话):psr/http-message本身并不是一个 HTTP 消息实现,它只是描述 HTTP 消息的接口规范。真正提供实现的是guzzlehttp/psr7这类实现包——本仓库中就内置了完整的 Guzzle 实现(upload/system/storage/vendor/guzzlehttp/psr7/src/,包含Message.php、Request.php、ServerRequest.php、Response.php、Stream.php、Uri.php、UploadedFile.php等)。而 OpenCart 的 Composer 自动加载器也把这些命名空间映射到位:
upload/system/storage/vendor/composer/autoload_psr4.php中Psr\Http\Message\命名空间同时指向psr/http-factory/src与psr/http-message/src;upload/system/storage/vendor/composer/installed.json中记录了psr/http-message的安装信息(install-path 为../psr/http-message)。
1.1 七大接口一览
PSR-7 规范一共定义了 7 个接口,upload/system/storage/vendor/psr/http-message/docs/PSR7-Interfaces.md给出的对照表如下:
| 接口名 | 职责描述 |
|---|---|
Psr\Http\Message\MessageInterface | 一条 HTTP 消息的抽象表示(请求与响应共有的部分) |
Psr\Http\Message\RequestInterface | 客户端发出的请求(outgoing, client-side request) |
Psr\Http\Message\ServerRequestInterface | 服务器接收到的请求(incoming, server-side request) |
Psr\Http\Message\ResponseInterface | 服务器发出的响应(outgoing, server-side response) |
Psr\Http\Message\StreamInterface | 描述一条数据流(data stream) |
Psr\Http\Message\UriInterface | 表示 URI 的值对象(value object) |
Psr\Http\Message\UploadedFileInterface | 通过 HTTP 请求上传的文件的值对象 |
1.2 不可变性(Immutability)原则
这是 PSR-7 最核心的语义,源码 docblock 中写得很明确(upload/system/storage/vendor/psr/http-message/src/MessageInterface.php):
Messages are considered immutable; all methods that might change state MUST be implemented such that they retain the internal state of the current message and return an instance that contains the changed state.
即:消息是不可变的。所有可能改变状态的方法(如withHeader、withBody、withStatus)都必须保留当前消息的内部状态,并返回一个包含新状态的新实例,而不是修改原对象。因此所有with*方法的返回值都必须被接收($response = $response->withHeader(...)),这一点在实际编码中是最容易踩的坑。
二、接口方法速查表(Cheatsheet)
PSR7-Interfaces.md明确说明其用途是"帮助在使用 PSR-7 时快速查找方法"。为方便读者查阅,以下完整保留各接口的方法清单。
2.1MessageInterface方法(请求与响应共有的方法)
| 方法名 | 说明 | 备注 |
|---|---|---|
getProtocolVersion() | 获取 HTTP 协议版本 | 如 1.0 或 1.1 |
withProtocolVersion($version) | 返回设置新协议版本后的新消息实例 | |
getHeaders() | 获取全部 HTTP 头 | 键为头名,值为字符串数组;保留原始大小写 |
hasHeader($name) | 检查指定头是否存在 | 头名大小写不敏感 |
getHeader($name) | 获取单个头的值数组 | 不存在时返回空数组 |
getHeaderLine($name) | 获取单个头以逗号拼接的字符串 | 不存在时返回空字符串 |
withHeader($name, $value) | 返回设置/替换指定头后的新实例 | 若原消息中已有该头则整体替换其值 |
withAddedHeader($name, $value) | 返回向指定头追加值后的新实例 | 头已存在则追加,不存在则新建 |
withoutHeader($name) | 返回移除指定头后的新实例 | 头名大小写不敏感 |
getBody() | 获取消息体 | 返回实现StreamInterface的对象 |
withBody(StreamInterface $body) | 返回设置新消息体后的新实例 |
2.2RequestInterface方法
包含MessageInterface全部方法,另加:
| 方法名 | 说明 | 备注 |
|---|---|---|
getRequestTarget() | 获取请求目标 | origin-form / absolute-form / authority-form / asterisk-form(RFC 7230) |
withRequestTarget($requestTarget) | 返回设置请求目标后的新实例 | |
getMethod() | 获取 HTTP 方法 | GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE(RFC 7231)及 PATCH(RFC 5789) |
withMethod($method) | 返回设置方法后的新实例 | |
getUri() | 获取 URI 实例 | |
withUri(UriInterface $uri, $preserveHost = false) | 返回设置 URI 后的新实例 |
2.3ServerRequestInterface方法
包含RequestInterface全部方法,另加:
| 方法名 | 说明 | 备注 |
|---|---|---|
getServerParams() | 获取服务器参数 | 通常来源于$_SERVER |
getCookieParams() | 获取客户端发送的 Cookie | 通常来源于$_COOKIE |
withCookieParams(array $cookies) | 返回设置 Cookie 后的新请求实例 | |
withQueryParams(array $query) | 返回设置查询字符串参数后的新实例 | |
getUploadedFiles() | 获取规范化后的文件上传数据 | |
withUploadedFiles(array $uploadedFiles) | 返回设置上传文件后的新实例 | |
getParsedBody() | 获取请求体中的参数 | |
withParsedBody($data) | 返回设置请求体参数后的新实例 | |
getAttributes() | 获取从请求派生的属性 | |
getAttribute($name, $default = null) | 获取单个派生属性 | |
withAttribute($name, $value) | 返回设置派生属性后的新实例 | |
withoutAttribute($name) | 返回移除派生属性后的新实例 |
2.4ResponseInterface方法
包含MessageInterface全部方法,另加:
| 方法名 | 说明 |
|---|---|
getStatusCode() | 获取响应状态码 |
withStatus($code, $reasonPhrase = '') | 返回设置状态码(可带原因短语)后的新实例 |
getReasonPhrase() | 获取状态码对应的原因短语 |
2.5StreamInterface方法
| 方法名 | 说明 |
|---|---|
__toString() | 从流头到尾读取全部数据为字符串 |
close() | 关闭流及底层资源 |
detach() | 将底层资源与流分离 |
getSize() | 获取流大小(未知则返回 null) |
tell() | 获取文件读写指针当前位置 |
eof() | 是否已到流末尾 |
isSeekable() | 流是否可定位 |
seek($offset, $whence = SEEK_SET) | 将指针定位到指定位置 |
rewind() | 将指针定位到流开头(等价于seek(0)) |
isWritable() | 流是否可写 |
write($string) | 向流写入数据,返回写入字节数 |
isReadable() | 流是否可读 |
read($length) | 从流读取至多$length字节 |
getContents() | 返回流中剩余内容组成的字符串 |
getMetadata($key = null) | 获取流元数据(键与stream_get_meta_data()一致) |
2.6UriInterface方法
| 方法名 | 说明 |
|---|---|
getScheme()/withScheme($scheme) | 获取 / 设置 URI 协议(scheme) |
getAuthority() | 获取 authority 组件(userinfo + host + port) |
getUserInfo()/withUserInfo($user, $password = null) | 获取 / 设置用户信息 |
getHost()/withHost($host) | 获取 / 设置主机 |
getPort()/withPort($port) | 获取 / 设置端口 |
getPath()/withPath($path) | 获取 / 设置路径 |
getQuery()/withQuery($query) | 获取 / 设置查询字符串 |
getFragment()/withFragment($fragment) | 获取 / 设置片段 |
__toString() | 返回 URI 引用的字符串表示 |
2.7UploadedFileInterface方法
| 方法名 | 说明 |
|---|---|
getStream() | 获取表示上传文件的流 |
moveTo($targetPath) | 将上传文件移动到新位置 |
getSize() | 获取文件大小 |
getError() | 获取与上传文件相关的错误码 |
getClientFilename() | 获取客户端发送的文件名 |
getClientMediaType() | 获取客户端发送的媒体类型 |
三、HTTP 头的操作实战
原文档(docs/PSR7-Usage.md)中的示例均假设:$request是Psr\Http\Message\RequestInterface的对象;$response是实现响应接口的对象(原文档此处笔误写成RequestInterface,实际应为ResponseInterface,详见文末勘误)。运行这些示例至少需要一个 PSR-7 实现包(例如 zendframework/zend-diactoros、guzzlehttp/psr7、slim/slim 等),所有实现的行为应一致。本仓库内置的就是guzzlehttp/psr7。
3.1 向响应添加响应头
$response->withHeader('My-Custom-Header', 'My Custom Message');注意:withHeader返回新实例,请务必接收返回值($response = $response->withHeader(...)),否则原响应对象不会有任何变化。
3.2 向已有头追加值
$response->withAddedHeader('My-Custom-Header', 'The second message');withAddedHeader与withHeader的区别在于:若头已存在,withAddedHeader保留旧值并追加新值;而withHeader会整体替换旧值。二者都会返回新实例(接口源码MessageInterface.php中对应 docblock 明确标注了这一点)。
3.3 检查头是否存在
$request->hasHeader('My-Custom-Header'); // 返回 false $response->hasHeader('My-Custom-Header'); // 返回 true注意:
My-Custom-Header只被添加到了 Response 中,因此对$request查询返回false,对$response查询返回true。头名匹配是大小写不敏感的(hasHeader的 docblock 明确要求使用 case-insensitive 比较)。
3.4 获取头值的逗号拼接字符串(同样适用于请求)
// 从请求头中取值 $request->getHeaderLine('Content-Type'); // 返回: "text/html; charset=UTF-8" // 从响应头中取值 $response->getHeaderLine('My-Custom-Header'); // 返回: "My Custom Message; The second message"getHeaderLine()将同一个头的所有值用逗号拼接成一个字符串。不过MessageInterface.php的 docblock 也提醒:并非所有头都适合用逗号拼接表示(例如 Cookie、Set-Cookie 等),这类头应使用getHeader()自行选择分隔符拼接。
3.5 获取头值的数组形式(同样适用于请求)
// 从请求头中取值 $request->getHeader('Content-Type'); // 返回: ["text/html", "charset=UTF-8"] // 从响应头中取值 $response->getHeader('My-Custom-Header'); // 返回: ["My Custom Message", "The second message"]getHeader()返回字符串数组,保留了每个值原本的形态;如果头不存在则返回空数组。注意与getHeaderLine()的返回类型差异:一个是string[],一个是string。
3.6 从头中移除响应头
// 从 Request 中移除头,例如移除已废弃的 "Content-MD5" 头 $request->withoutHeader('Content-MD5'); // 从 Response 中移除头 // 效果:浏览器将无法获知流的长度 // 浏览器会一直下载到流结束为止 $response->withoutHeader('Content-Length');移除Content-Length会让响应变成"未知长度"的分块/流式输出,浏览器会持续读取直到流结束——这在流式响应场景下是一种常见的主动行为。
3.7 头操作底层语义小结(源码依据)
结合upload/system/storage/vendor/psr/http-message/src/MessageInterface.php的实现约定:
- 头名匹配一律大小写不敏感,但
getHeaders()与withHeader()会保留最初指定的大小写,即"写时保留、读时忽略大小写"; withHeader($name, $value)的$value支持string|string[],非法头名或非法值会抛出\InvalidArgumentException;- 所有
with*/without*方法都必须维持不可变性并返回新实例。
四、HTTP 消息体(Body / Stream)的操作实战
使用 PSR-7 处理消息体有两种实现方式,原文档对此有明确推荐。
4.1 方式一:单独取出 Body 再操作
这种方式让 body 的处理更容易理解,在需要反复调用 body 方法时非常有用(只需调用一次
getBody())。这种方式还可以避免$response->write()这类误用。
$body = $response->getBody(); // 对 body 进行操作,例如 read、write、seek // ... // 用(可能被替换过的)新 body 替换旧 body $response->withBody($body); // 上面这条语句是可选的,因为我们操作的是对象 // 在这种情况下"新"body 与"旧"body 是同一个 // $body 变量与 $request 中的值相同,只是传入了引用这里withBody($body)之所以"可选",是因为 PHP 中对象默认按引用传递:你通过$body对流的修改会直接反映在$response内部持有的同一个流对象上。因此只有在确实想替换成另一个流对象时,withBody()才是必须的。
4.2 方式二:直接在 Response 上操作
这种方式在只做少量操作时比较方便,因为不需要
$request->getBody()这行语句。
$response->getBody()->write('hello');4.3 获取消息体内容
下面的代码片段获取流的内容。注意流指针的语义:
流必须被 rewind(回卷)到开头。如果之前向流中写入过内容,直接调用
getContents()会忽略已写入的内容——因为写入后流指针停在最后一个字符位置(\0),意味着已到流末尾。
$body = $response->getBody(); $body->rewind(); // 或者 $body->seek(0); $bodyText = $body->getContents();注意:如果在
getContents()之前调用了$body->seek(1),那么第一个字符会被跳过,因为起始指针被设为1而不是0。这就是为什么推荐使用$body->rewind()。
结合StreamInterface.php源码 docblock 可进一步理解:getContents()返回的是"流中剩余的内容"(Returns the remaining contents in a string),它从当前指针位置读到流末尾;而__toString()则相反,会先尝试 seek 到流开头再从头读到尾。
4.4 向 Body 追加内容
$response->getBody()->write('Hello'); // 直接写入 $body = $request->getBody(); // 这是一个 StreamInterface 对象 $body->write('xxxxx');write()的行为遵循底层 PHP 流(如fwrite)的语义:如果指针当前不在流末尾,则从指针当前位置覆盖写入;如果指针在末尾,则表现为追加。
五、流指针(Stream Pointer)的进阶语义与"前置内容"
前置(prepend)操作对流来说与追加完全不同:必须先读出原内容,再从头写入"前置部分 + 原内容"。下面的示例解释流的这一行为。
// 假设我们的 response 初始为空 $body = $response->getBody(); // 写入字符串 "abcd" $body->write('abcd'); // 将指针定位到流开头 $body->seek(0); // 写入 'ef' $body->write('ef'); // 此时流的内容是 "efcd"关键点:seek(0)之后write('ef')是覆盖式写入,从位置 0 开始覆盖两个字符ab,因此结果是efcd(cd保留),而不是efabcd。这正是"流的写入位置由指针决定"的直观体现。
5.1 通过单独重写实现前置
// 假设我们的 response body 流中只包含: "abcd" $body = $response->getBody(); $body->rewind(); $contents = $body->getContents(); // abcd // 将流指针定位到开头 $body->rewind(); $body->write('ef'); // 流内容变为 "efcd" $body->write($contents); // 流内容变为 "efabcd"注意:
getContents()在读取时会移动流指针,因此如果缺少第二次rewind(),流会变成abcdefabcd——因为write()在未被rewind()或seek(0)前置的情况下,会从当前指针位置(流末尾)继续追加写入。
5.2 先把内容拼成字符串再整体写回
$body = $response->getBody(); $body->rewind(); $contents = $body->getContents(); // efabcd $contents = 'ef' . $contents; $body->rewind(); $body->write($contents);这种方式只做一次write(),逻辑更清晰:读出全部内容 → 在字符串层面拼接前置部分 → 回卷指针 → 整体覆盖写回。
5.3 为什么推荐rewind()而非seek(0)
两种写法效果等价(rewind()内部就是seek(0),见StreamInterface.phpdocblock),但rewind()语义更明确、不易出错——你不需要记住seek()的偏移量与SEEK_SET等whence参数。seek($offset, $whence = SEEK_SET)的三个whence取值与 PHP 内置fseek()完全一致:SEEK_SET(绝对偏移)、SEEK_CUR(相对当前偏移)、SEEK_END(相对流末尾偏移)。
六、仓库内参考实现:guzzlehttp/psr7 与配套 PSR 包
psr/http-message只是接口契约,要真正运行上面所有示例,必须有实现。OpenCart 仓库中随附了完整的 PSR-7 实现与配套接口,可直接查看源码加深理解:
- 实现包:
upload/system/storage/vendor/guzzlehttp/psr7/src/,其中Request.php、ServerRequest.php、Response.php分别实现对应接口,Stream.php是流的核心实现(对 PHP 底层 stream 资源进行封装),MessageTrait.php承载头操作与协议版本等公共逻辑,Uri.php、UploadedFile.php对应 URI 与上传文件接口。例如AppendStream、BufferStream、LimitStream、CachingStream、InflateStream等类都直接implements StreamInterface,可用于构造复合流场景。 - 工厂接口:
upload/system/storage/vendor/psr/http-factory/src/定义了RequestFactoryInterface、ResponseFactoryInterface、ServerRequestFactoryInterface、StreamFactoryInterface、UploadedFileFactoryInterface、UriFactoryInterface,用于在不依赖具体实现类的情况下创建 PSR-7 对象(依赖注入友好)。 - HTTP 客户端接口:
upload/system/storage/vendor/psr/http-client/src/定义了ClientInterface(sendRequest())及异常接口,是 PSR-18 的 HTTP 客户端契约。 - 主要消费方:仓库内
guzzlehttp/guzzle与aws/aws-sdk-php都依赖这些 PSR 接口(例如upload/system/storage/vendor/aws/aws-sdk-php/composer.json中声明了"psr/http-message": "^1.0 || ^2.0")。从依赖结构看,可以推断 OpenCart 扩展开发中调用 AWS 云服务或外部 REST API 时,正是通过这些 PSR-7 接口与 Guzzle 实现完成 HTTP 交互。
若想在本地验证自动加载与接口可用性,可执行:
php -r "require 'upload/system/vendor.php'; var_dump(interface_exists('Psr\\Http\\Message\\ResponseInterface'));"返回bool(true)即表示 PSR-7 接口已随 Composer 自动加载就绪(upload/system/vendor.php是仓库的 Composer 引导文件)。
七、易错点速查(写给快速上手的开发者)
- 忘记接收
with*返回值:不可变性意味着$response->withHeader(...)不会修改$response,必须写$response = $response->withHeader(...)。 getContents()前忘记rewind():读取前指针不在开头,会丢掉前面的内容或得到空字符串。write()前不定位指针:指针在末尾则追加、在中间则覆盖,结果取决于调用历史——必要时先rewind()或seek(0)。- 用
getHeaderLine()拼接不适合逗号拼接的头:Cookie、Set-Cookie 等头应使用getHeader()自行处理。 - 混淆
withHeader与withAddedHeader:前者整体替换,后者追加保留旧值。
八、原文档勘误说明
原文档docs/PSR7-Usage.md中有两处笔误,阅读时请注意:
- 前置假设部分写着
$responseis an object implementingPsr\Http\Message\RequestInterface,根据上下文与接口继承关系(响应应实现ResponseInterface),此处应为ResponseInterface; - "Prepend to body" 示例中
$repsonse->getBody()为response的拼写错误。
结语与延伸阅读
PSR-7 的接口契约 + 不可变语义 + 流指针模型,是理解现代 PHP HTTP 客户端与中间件体系的地基。本文覆盖了原文档docs/PSR7-Usage.md的全部示例(头操作、体操作、追加/前置、指针陷阱),并补齐了配套文档docs/PSR7-Interfaces.md的完整方法速查与接口源码依据。仓库内可供继续深挖的关键文件:
- 用法指南:upload/system/storage/vendor/psr/http-message/docs/PSR7-Usage.md
- 接口速查:upload/system/storage/vendor/psr/http-message/docs/PSR7-Interfaces.md
- 核心接口源码:MessageInterface.php、StreamInterface.php
- 参考实现:guzzlehttp/psr7 源码目录
- 自动加载映射:composer/autoload_psr4.php
- 电商
- 后端
【免费下载链接】opencart
A free shopping cart system. OpenCart is an open source PHP-based online e-commerce solution.
相关推荐
ShowDoc 中的 PSR-7 HTTP 消息实战:psr/http-message 消息头与流式消息体操作指南
ShowDoc 中的 PSR 7 HTTP 消息实战:psr/http message 消息头与流式消息体操作指南 导读 本文基于 ShowDoc 仓库内 ps
文档知识库后端前端技术深度解析:Electrobun 如何用 Bun 和 Zig 重塑桌面应用性能边界
技术深度解析:Electrobun 如何用 Bun 和 Zig 重塑桌面应用性能边界 在当今桌面应用开发领域,性能与开发效率的平衡始终是一个技术挑战。传统 El
桌面应用跨平台BiliBiliToolPro完整部署指南:5分钟快速搭建B站自动化任务助手
BiliBiliToolPro完整部署指南:5分钟快速搭建B站自动化任务助手 BiliBiliToolPro是一款功能强大的B站自动任务工具,能够帮助用户自动完
后端任务调度工作流自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考