- 后端
【免费下载链接】psr7
PSR-7 HTTP message library
导读
本文以guzzlehttp/psr7的官方文档 docs/psr-7-messages.md 为骨架,系统讲解 PSR-7 消息对象体系:Request、Response、ServerRequest、UploadedFile以及消息级的 Header、URI、Body API。读完本文你将掌握如何创建与加工 HTTP 消息、如何从 PHP 超全局变量还原服务端请求、如何解析复杂响应头、如何正确处理文件上传,并理解这些对象在 Guzzle、PSR-18 客户端、PSR-15 中间件等生态中自由流转的底层机制。
PSR-7 消息模型与不可变性
HTTP 请求与响应在 PSR-7 中都是「消息」(Message)。一条消息由三部分组成:起始行(start line)、头部(headers)、可选的消息体流(body stream)。请求的起始行包含方法、请求目标和协议版本,响应的起始行包含协议版本、状态码和原因短语。
本包提供的消息对象(Request、Response、ServerRequest)均实现 PSR-7 对应接口,可以在 Guzzle、PSR-18 客户端、PSR-15 中间件及其他 PSR-7 兼容库之间自由传递。
消息与 URI 对象都是不可变(immutable)的:所有以with*()开头的方法不会修改原对象,而是返回一份修改后的副本。例如withHeader()、withUri()、withStatus()等。而消息体流是可变的句柄,读取、写入、seek 都会改变其游标或内容。关于流的细节见 Streams and Decorators,URI 辅助函数见 URI Helpers。
以源码实现为例,MessageTrait(src/MessageTrait.php)中withHeader()通过clone $this生成新对象再写入头部;withProtocolVersion()在版本一致时甚至直接返回自身以节省开销,这正是不可变对象的标准实现模式。
创建 Request 请求对象
使用GuzzleHttp\Psr7\Request创建请求:
use GuzzleHttp\Psr7\Request; $request = new Request('GET', 'https://example.com/users/123'); // 也可以提供可选的头和消息体 $headers = ['Accept' => 'application/json']; $body = 'request body'; $request = new Request('PUT', 'https://example.com/users/123', $headers, $body);从构造函数签名(src/Request.php)可以看到完整参数顺序为:
new Request( string $method, // HTTP 方法 $uri, // string | UriInterface array $headers = [], // (string|string[])[] 头部映射 $body = null, // string | resource | StreamInterface string $version = '1.1' // 协议版本,默认 1.1 );构造过程中的几个关键点:
- URI 字符串自动解析:当
$uri不是UriInterface实例时,会被new Uri($uri)包装(src/Request.php); - 方法名校验:方法必须符合 RFC 9110 的 token 规则,非法方法会抛出
InvalidArgumentException(src/Request.php); - Host 头自动填充:如果构造时没有显式提供
host头,会从 URI 的 host/port 自动生成 Host 头,并且按 RFC 9110 第 7.2 节的要求保证 Host 是第一个头(src/Request.php); - 请求目标(request-target)推导:从 URI 的 path 和 query 组成 origin-form 目标,空路径被规范为
/(src/Request.php)。
创建 Response 响应对象
使用GuzzleHttp\Psr7\Response创建响应:
use GuzzleHttp\Psr7\Response; // 构造函数不要求任何参数 $response = new Response(); echo $response->getStatusCode(); // 200 echo $response->getProtocolVersion(); // 1.1 // 可以提供状态码、头、消息体和协议版本 $response = new Response(200, ['Content-Type' => 'application/json'], '{"ok":true}', '1.1');构造函数完整签名(src/Response.php)还包含第五个可选参数?string $reason,用于自定义原因短语:
new Response( int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null );从源码结构可以确认两点实现细节:
- 状态码范围校验:状态码必须是 100~599 之间的整数,否则抛出异常(src/Response.php);
- 原因短语自动映射:包内维护了一张标准状态码到原因短语的映射表
PHRASES(涵盖 100~511 共 60 余个标准状态码,src/Response.php)。当未提供$reason或提供的为''时,会自动按状态码查表生成,例如 200 对应OK、404 对应Not Found。因此响应对象的三个核心读取方法总是可用:getStatusCode()、getReasonPhrase()、getProtocolVersion()。
创建 ServerRequest 服务端请求对象
服务端请求(ServerRequest)代表服务器侧收到的入站 HTTP 请求。它在普通请求的方法、URI、头、消息体之外,还额外携带服务器参数、Cookie、查询参数、已解析的消息体数据、属性(attributes)和上传文件。
use GuzzleHttp\Psr7\ServerRequest; $request = new ServerRequest('POST', 'https://example.com/form', [], 'name=Guzzle', '1.1', [ 'REMOTE_ADDR' => '192.0.2.1', ]); $request = $request ->withCookieParams(['session' => 'abc']) ->withQueryParams(['page' => '1']) ->withParsedBody(['name' => 'Guzzle']) ->withAttribute('route', 'profile'); echo $request->getServerParams()['REMOTE_ADDR']; echo $request->getCookieParams()['session']; echo $request->getQueryParams()['page']; echo $request->getParsedBody()['name']; echo $request->getAttribute('route');构造函数与Request几乎一致,仅多出第六个参数array $serverParams = [](通常传入$_SERVER,src/ServerRequest.php)。注意该参数与 Cookie 参数在源码中都被标记了#[\SensitiveParameter],用于避免敏感信息进入异常堆栈或日志。
ServerRequest继承了Request的全部能力,同时提供一组with*()/get*()配对方法(src/ServerRequest.php):
| 方法对 | 语义 |
|---|---|
withServerParams()/getServerParams() | 服务器环境参数,如REMOTE_ADDR、SERVER_NAME |
withCookieParams()/getCookieParams() | 请求 Cookie 数组 |
withQueryParams()/getQueryParams() | 查询参数(通常来自$_GET) |
withParsedBody()/getParsedBody() | 已解析的消息体(数组、对象或 null,传入其他类型会抛异常) |
withAttribute()/getAttribute($name, $default)/withoutAttribute() | 应用层属性,常用于路由匹配结果、中间件上下文传递;属性不存在时返回默认值 |
fromGlobals():从 PHP 超全局变量还原请求
ServerRequest::fromGlobals()从 PHP 超全局变量一次性构建服务端请求。它读取$_SERVER、$_GET、$_POST、$_COOKIE和$_FILES,并在可用时尝试还原请求头:
use GuzzleHttp\Psr7\ServerRequest; $request = ServerRequest::fromGlobals();从源码看,fromGlobals()委托给内部的ServerRequestGlobalsFactory::fromArrays()(src/ServerRequestGlobalsFactory.php),其还原逻辑包括:
- 方法:从
$_SERVER['REQUEST_METHOD']读取,缺失时默认GET,并统一转为大写(见后文「HTTP 方法大小写」一节); - 头:优先使用
apache_request_headers()(若函数存在),否则从HTTP_*、CONTENT_TYPE、CONTENT_LENGTH、CONTENT_MD5等键还原,还会把REDIRECT_HTTP_AUTHORIZATION、PHP_AUTH_USER/PHP_AUTH_PW(转为 Basic 认证)、PHP_AUTH_DIGEST组装成Authorization头(src/ServerRequestGlobalsFactory.php); - URI:综合
HTTPS、HTTP_HOST、SERVER_NAME/SERVER_ADDR、SERVER_PORT、REQUEST_URI、QUERY_STRING拼装,并支持 CONNECT authority-form、absolute-form、asterisk-form 等多种请求目标形态(src/ServerRequestGlobalsFactory.php); - 消息体:使用
php://input包装成可缓存流CachingStream(src/ServerRequestGlobalsFactory.php); - 协议版本:从
SERVER_PROTOCOL解析,默认1.1。
集成测试 tests/Integration/ServerRequestFromGlobalsTest.php 通过真实的 HTTP 服务器验证了fromGlobals()能正确还原 method、headers、body 等字段。
getUriFromGlobals():仅提取 URI
如果只需要从$_SERVER推导 URI,使用ServerRequest::getUriFromGlobals():
use GuzzleHttp\Psr7\ServerRequest; $uri = ServerRequest::getUriFromGlobals();该方法同样由ServerRequestGlobalsFactory::getUriFromServerParams($_SERVER)实现(src/ServerRequestGlobalsFactory.php)。URI 的构造与规范化辅助函数,详见 URI Helpers。
请求对象速览:方法与 URI
use GuzzleHttp\Psr7\Request; $request = new Request('GET', 'https://example.com/users/123', [ 'Accept' => 'application/json', ]); echo $request->getMethod(); echo $request->getUri();PSR-7 消息不可变。withHeader()、withUri()等返回修改后的副本:
$jsonRequest = $request->withHeader('Accept', 'application/json');值得留意的是withUri()还接受第二个布尔参数$preserveHost:为true时保留原有 Host 头而不从新 URI 覆盖(src/Request.php),这在代理场景下非常有用。
响应对象速览:状态、头与消息体
use GuzzleHttp\Psr7\Response; $response = new Response(200, ['Content-Type' => 'application/json'], '{"ok":true}'); echo $response->getStatusCode(); echo $response->getHeaderLine('Content-Type'); echo $response->getBody();getHeaderLine()会把同名多值头用,拼接成单行字符串(src/MessageTrait.php),适合直接展示;getBody()返回消息体流实例。
URI 对象速览
use GuzzleHttp\Psr7\Uri; $uri = new Uri('https://example.com/users?active=1'); echo $uri->getHost(); echo $uri->getQuery();Uri是本包对Psr\Http\Message\UriInterface的完整实现(src/Uri.php),构造时由UriParser按 RFC 3986 解析,解析失败抛出MalformedUriException(src/Uri.php)。URI 特有的辅助方法(比较、规范化、解析等)见 URI Helpers。
Headers 头部操作
请求和响应消息都包含 HTTP 头。头部存储采用「小写名 → 原始名」的映射结构(src/MessageTrait.php),因此hasHeader()、getHeader()对大小写不敏感,同时又能保持原始书写风格。
检查与读取头部
使用hasHeader()检查消息是否包含某个头:
use GuzzleHttp\Psr7\Request; $request = new Request('GET', '/', ['X-Foo' => 'bar']); if ($request->hasHeader('X-Foo')) { echo 'It is there'; }用getHeader()获取某头的全部值(字符串数组):
$request->getHeader('X-Foo'); // ['bar'] // 缺失的头返回空数组 $request->getHeader('X-Bar'); // []用getHeaders()遍历消息的所有头:
foreach ($request->getHeaders() as $name => $values) { echo $name . ': ' . implode(', ', $values) . "\r\n"; }从实现看,getHeaders()返回的是原始大小写名称到值数组的映射,getHeader()对缺失头返回[](src/MessageTrait.php)。此外构造消息时若同名头出现多次,值会被自动合并进同一个头的值数组(src/MessageTrait.php)。
解析复杂头(Complex Headers)
某些头包含额外的键值对信息。例如Link头除了链接本身还携带参数:
<https://example.com/front.jpeg>; rel="front"; type="image/jpeg"使用GuzzleHttp\Psr7\Header::parse()解析这类头:
use GuzzleHttp\Psr7\Header; use GuzzleHttp\Psr7\Request; $request = new Request('GET', '/', [ 'Link' => '<https://example.com/front.jpeg>; rel="front"; type="image/jpeg"', ]); $parsed = Header::parse($request->getHeader('Link')); var_export($parsed);输出结果:
array ( 0 => array ( 0 => '<https://example.com/front.jpeg>', 'rel' => 'front', 'type' => 'image/jpeg', ), )结果由键值对组成:没有键的头值(如裸链接)按数字索引存放,而构成键值对的部分则以其参数名作为键。从源码看,Header::parse()先按逗号切分列表值(splitList()),再按分号切分参数(splitParameters()),过程中会正确跳过引号内和反斜杠转义的内容(src/Header.php)。splitList()同样可以独立使用,用于拆分accept、cache-control、if-none-match这类逗号分隔的头(但不能用于user-agent、set-cookie等非列表头,src/Header.php)。
Body 消息体
请求和响应的消息体都是Psr\Http\Message\StreamInterface实例。流既用于上传数据,也用于下载数据:
use GuzzleHttp\Psr7\Response; $response = new Response(200, [], 'response body'); echo $response->getBody(); // response body消息体可以整体转成字符串,也可以按需读取字节:
$body = $response->getBody(); echo $body->read(4); $body->seek(0); echo $body->getContents();注意:echo $response->getBody()实际触发的是流的__toString(),而getBody()本身返回的是流对象。这里体现了 PSR-7 的核心设计——消息是值对象,消息体是可变句柄:read(4)会推进游标,因此读取前需要用seek(0)回到开头,getContents()才返回完整内容。
关于流的创建与装饰器(streamFor()、LimitStream、CachingStream等)的更多示例,见 Streams and Decorators。
Uploaded Files 上传文件
上传文件由Psr\Http\Message\UploadedFileInterface实例表示。本包提供的GuzzleHttp\Psr7\UploadedFile可以包装本地文件路径、PHP 流资源或 PSR-7 流三种数据源:
use GuzzleHttp\Psr7\UploadedFile; use GuzzleHttp\Psr7\Utils; $stream = Utils::streamFor('file contents'); $upload = new UploadedFile($stream, $stream->getSize(), UPLOAD_ERR_OK, 'example.txt', 'text/plain'); echo $upload->getClientFilename(); echo $upload->getClientMediaType(); echo $upload->getSize();构造函数签名(src/UploadedFile.php)为:
new UploadedFile( $streamOrFile, // StreamInterface | string(路径) | resource ?int $size, int $errorStatus, // PHP 的 UPLOAD_ERR_* 常量 ?string $clientFilename = null, ?string $clientMediaType = null );从源码可以确认:当传入的是路径字符串时,getStream()会按需以r+模式惰性打开(LazyOpenStream,src/UploadedFile.php);传入的资源会被包装为Stream。
调用getStream()读取上传内容,或调用moveTo()把文件移动/复制到目标路径。moveTo()成功之后,isMoved()返回true,任何需要活动上传流的调用都会抛出异常:
$body = $upload->getStream(); echo $body->getContents(); $upload->moveTo('/path/to/target.txt'); var_export($upload->isMoved()); // truemoveTo()的实现(src/UploadedFile.php)值得说明:
- 目标是空字符串会抛
InvalidArgumentException; - 底层为文件路径时,CLI 环境用
rename(),Web 环境用move_uploaded_file()(后者保证了安全性检查); - 底层为流时,先把流 rewind,再用
Utils::copyToStream()拷贝到目标文件; - 移动失败抛出
RuntimeException。
错误状态的处理:如果上传错误码不是UPLOAD_ERR_OK,对象仍然暴露getError()、getSize()、getClientFilename()、getClientMediaType(),但getStream()和moveTo()会抛出异常——因为此时并不存在成功上传的内容。源码通过validateActive()统一把关:错误码非 OK 或文件已被移动都会抛RuntimeException(src/UploadedFile.php),合法的错误码集合定义在ERROR_MAP中(UPLOAD_ERR_INI_SIZE、UPLOAD_ERR_FORM_SIZE、UPLOAD_ERR_PARTIAL、UPLOAD_ERR_NO_FILE等,src/UploadedFile.php)。
normalizeFiles():把 $_FILES 结构转成上传文件树
ServerRequest::normalizeFiles()把$_FILES风格的数组转换成上传文件实例的树形结构。它接受简单文件规格、嵌套的 PHP$_FILES形态、已有的UploadedFileInterface实例,以及上传文件的嵌套数组:
use GuzzleHttp\Psr7\ServerRequest; $files = ServerRequest::normalizeFiles([ 'avatar' => [ 'tmp_name' => '/tmp/php123', 'size' => 1024, 'error' => UPLOAD_ERR_OK, 'name' => 'avatar.png', 'type' => 'image/png', ], 'photos' => [ 'tmp_name' => [ 'first' => '/tmp/php456', ], 'size' => [ 'first' => 2048, ], 'error' => [ 'first' => UPLOAD_ERR_OK, ], 'name' => [ 'first' => 'photo.jpg', ], 'type' => [ 'first' => 'image/jpeg', ], ], ]); $request = (new ServerRequest('POST', '/upload'))->withUploadedFiles($files);上例中的photos正是 PHP$_FILES处理<input name="photos[first]">这类多文件上传时产生的嵌套形态:tmp_name、size、error等键内部再按索引拆分。
从源码看,normalizeFiles()委托给UploadedFileNormalizer::normalize()(src/ServerRequest.php),其递归逻辑为(src/UploadedFileNormalizer.php):
- 值已是
UploadedFileInterface→ 直接保留; - 值是含
tmp_name键的数组 → 视为文件规格,构建UploadedFile(tmp_name/size/error三个键必填,否则抛InvalidArgumentException,src/UploadedFileNormalizer.php); - 值是不含
tmp_name的数组 → 递归归一化(支持任意层级嵌套); - 其他类型 → 抛
InvalidArgumentException。
嵌套规格内部还要求tmp_name、size、error三个数组的键一一对应,否则视为非法(src/UploadedFileNormalizer.php)。
HTTP 方法大小写(Method Casing)
HTTP 方法名在 PSR-7 中区分大小写。通过Request、ServerRequest、withMethod()、Message::parseRequest()或 PSR-17 工厂显式创建的请求,会原样保留传入的方法字符串;只有ServerRequest::fromGlobals()在从 PHP 服务器全局变量灌入请求时,会把$_SERVER['REQUEST_METHOD']规范化为大写以保证兼容性——该行为由ServerRequestGlobalsFactory::getRequestMethodFromServer()中的Utils::asciiToUpper()实现(src/ServerRequestGlobalsFactory.php)。
Request Methods 请求方法
创建请求时提供要执行的方法。可以指定任意方法,包括 RFC 9110 未收录的自定义方法:
use GuzzleHttp\Psr7\Request; $request = new Request('MOVE', 'https://example.com/resource'); echo $request->getMethod(); // MOVEwithMethod()换方法同理,且会先经过 RFC 9110 token 校验(src/Request.php)。
Request URI 请求 URI
请求 URI 由Psr\Http\Message\UriInterface对象表示,本包通过GuzzleHttp\Psr7\Uri提供实现。
创建请求时,URI 既可以是字符串,也可以是UriInterface实例:
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; $request = new Request('GET', new Uri('https://example.com/users?id=123'));下面按 URI 组件逐个说明读取方式。
Scheme 协议
scheme 指明协议,HTTP 请求通常为http或https:
$request = new Request('GET', 'https://example.com'); echo $request->getUri()->getScheme(); // httpsHost 主机
主机既可从 URI 获取,也同时体现在Host头中:
$request = new Request('GET', 'https://example.com'); echo $request->getUri()->getHost(); // example.com echo $request->getHeaderLine('Host'); // example.com这正是前文提到的:Request构造时会从 URI 自动生成 Host 头并置于头部首位(src/Request.php)。
Port 端口
http和https的默认端口无需显式写出:
$request = new Request('GET', 'https://example.com:8443'); echo $request->getUri()->getPort(); // 8443Uri内部维护了一张默认端口表(http=80、https=443、ftp=21 等,src/Uri.php),因此getPort()只在端口非默认值时才返回非 null;Host 头的生成也会把非默认端口拼入(host:port)。
Path 路径
请求路径通过 URI 对象访问:
$request = new Request('GET', 'https://example.com/users/123'); echo $request->getUri()->getPath(); // /users/123URI 路径中不允许的字符会按 RFC 3986 section 3.3 进行百分号编码。此外,Request会把 URI 路径规范为 origin-form 请求目标:空路径变为/,以//开头的路径会被折叠以避免被解析成 network-path reference(src/Request.php)。
Query String 查询字符串
查询字符串同样通过 URI 对象访问:
$request = new Request('GET', 'https://example.com/?foo=bar'); echo $request->getUri()->getQuery(); // foo=bar查询串中不允许的字符会按 RFC 3986 section 3.4 或ServerRequest::getQueryParams()。
Response Status 响应状态
响应暴露状态码、原因短语和协议版本:
use GuzzleHttp\Psr7\Response; $response = new Response(200, [], 'OK'); echo $response->getStatusCode(); // 200 echo $response->getReasonPhrase(); // OK echo $response->getProtocolVersion(); // 1.1如前面「创建 Response」一节所述,getReasonPhrase()的返回值来自内置PHRASES映射表(200 →OK),未标准的状态码可以借助withStatus($code, $reasonPhrase)提供自定义短语;原因短语同样要经过 RFC 9112 校验,不能包含非法控制字符(src/Response.php)。
总结与延伸阅读
guzzlehttp/psr7的消息对象体系覆盖了 HTTP 消息处理的全部核心场景:以Request/Response表示客户端与服务端消息,以ServerRequest承载服务端入站数据(服务器参数、Cookie、查询、解析体、属性、上传文件),以UploadedFile封装文件上传,并通过不可变的with*()方法与可变的流式消息体,实现了消息在 Guzzle、PSR-18 客户端、PSR-15 中间件等 PSR-7 生态组件之间的无缝流转。
继续深入阅读仓库内相关文档:
- Streams and Decorators — 流的创建、读取与装饰器模式
- URI Helpers — URI 的解析、比较与规范化辅助函数
- Message Helpers — 消息字符串序列化、解析与摘要
- Header and Query Helpers — 头部与查询串的解析工具
- PSR-17 Factories — 基于工厂模式创建 PSR-7 消息
- Diagnostic Values — 安全转义与诊断值工具
- 后端
【免费下载链接】psr7
PSR-7 HTTP message library
相关推荐
Guzzle 与 PSR-7 集成实战指南:请求、响应、流与 PSR-17 工厂深度解析
Guzzle 与 PSR 7 集成实战指南:请求、响应、流与 PSR 17 工厂深度解析 Guzzle 是基于 PSR 7 接口构建的 PHP HTTP 客户端
后端Falcon ASGI 请求与响应对象全解析:falcon.asgi.Request / Response 实战指南
Falcon ASGI 请求与响应对象全解析:falcon.asgi.Request / Response 实战指南 在 Falcon 的 ASGI 应用中,
后端Web框架API设计如何3分钟获取阿里云盘Refresh Token:扫码实现自动化文件管理的终极指南
如何3分钟获取阿里云盘Refresh Token:扫码实现自动化文件管理的终极指南 阿里云盘Refresh Token获取工具是一款让普通用户也能轻松掌握云盘自
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考