☰
深入掌握 PSR-7:基于 OpenCart 内置 psr/http-message 的 HTTP 消息与流式操作实战
2026/9/27 21:31:54 网站建设 项目流程
  • 电商
  • 后端

【免费下载链接】opencart

A free shopping cart system. OpenCart is an open source PHP-based online e-commerce solution.

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

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 引导文件)。

七、易错点速查(写给快速上手的开发者)

  1. 忘记接收with*返回值:不可变性意味着$response->withHeader(...)不会修改$response,必须写$response = $response->withHeader(...)。
  2. getContents()前忘记rewind():读取前指针不在开头,会丢掉前面的内容或得到空字符串。
  3. write()前不定位指针:指针在末尾则追加、在中间则覆盖,结果取决于调用历史——必要时先rewind()或seek(0)。
  4. 用getHeaderLine()拼接不适合逗号拼接的头:Cookie、Set-Cookie 等头应使用getHeader()自行处理。
  5. 混淆withHeader与withAddedHeader:前者整体替换,后者追加保留旧值。

八、原文档勘误说明

原文档docs/PSR7-Usage.md中有两处笔误,阅读时请注意:

  1. 前置假设部分写着$responseis an object implementingPsr\Http\Message\RequestInterface,根据上下文与接口继承关系(响应应实现ResponseInterface),此处应为ResponseInterface;
  2. "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.

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

相关推荐

上一篇:终极番茄钟计时器:免费开源工具让你告别拖延症
下一篇:终极RKNN模型部署实战:从零到一的高效AI应用指南

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

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

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

立即咨询