PSR-7元文档

HTTP消息元文档

1.总结

此提议的目的是为RFC 7230RFC 7231中描述的HTTP消息提供一组公共接口,并在RFC 3986(在HTTP消息的上下文中)中描述URI

  • RFC 7230:http://www.ietf.org/rfc/rfc7230.txt
  • RFC 7231:http://www.ietf.org/rfc/rfc7231.txt
  • RFC 3986:http://www.ietf.org/rfc/rfc3986.txt

所有HTTP消息都包含正在使用的HTTP协议版本,标头和消息正文。请求建立在消息,以包括用于使请求的HTTP方法,以及其中所述请求是由所述URI。响应包括HTTP状态代码和原因短语。

在PHP中,HTTP消息在两个上下文中使用:

  • 通过ext/curl扩展,PHP的本地流层等发送HTTP请求,并处理收到的HTTP响应。换句话说,当使用PHP作为HTTP客户端时,使用HTTP消息
  • 处理传入的HTTP请求到服务器,并向发出请求的客户端返回HTTP响应。当用作服务器端应用程序来完成HTTP请求时,PHP可以使用HTTP消息

该提议提供了一个API,用于完整描述PHP中各种HTTP消息的所有部分。

2. PHP中的HTTP消息

PHP没有内置的HTTP消息支持。

客户端HTTP支持

PHP支持通过以下几种机制发送HTTP请求:

PHP流是发送HTTP请求的最方便和最普遍的方式,但在正确配置SSL支持方面存在许多限制,并且在设置诸如头之类的东西时提供了麻烦的界面。cURL提供了完整且扩展的功能集,但由于它不是默认扩展名,因此通常不存在。http扩展程序遇到与cURL相同的问题,以及它传统上使用的示例少得多。

大多数现代HTTP客户端库倾向于抽象实现,以确保它们可以在任何上面执行的任何环境中工作。

服务器端HTTP支持

PHP使用服务器API(SAPI)来解释传入的HTTP请求,编组输入以及将处理传递给脚本。最初的SAPI设计镜像了通用网关接口,它可以在将委托传递给脚本之前封送请求数据并将其推送到环境变量中; 然后,脚本将从环境变量中提取,以处理请求并返回响应。

PHP的SAPI设计通过superglobals(,和,分别)提取常见的输入源,如cookie,查询字符串参数和url编码的POST内容$_COOKIE为Web开发人员提供了一层便利。$_GET$_POST

在等式的响应方面,PHP最初是作为模板语言开发的,并允许混合HTML和PHP; 文件的任何HTML部分都会立即刷新到输出缓冲区。现代应用程序和框架避开了这种做法,因为它可能导致发出状态行和/或响应头的问题; 它们倾向于聚合所有标题和内容,并在所有其他应用程序处理完成时立即发出它们。需要特别注意确保错误报告和将内容发送到输出缓冲区的其他操作不会刷新输出缓冲区。

为什么要打扰?

HTTP消息用于许多PHP项目 - 客户端和服务器。在每种情况下,我们都会观察以下一种或多种模式或情况:

  1. 项目直接使用PHP的超级全局。
  2. Projects将从头开始创建实现。
  3. 项目可能需要提供HTTP消息实现的特定HTTP客户端/服务器库。
  4. 项目可以为常见的HTTP消息实现创建适配器。

例如:

  1. 几乎任何在框架兴起之前开始开发的应用程序,其中包括许多非常流行的CMS,论坛和购物车系统,历史上都使用了超级全局。
  2. 诸如Symfony和Zend Framework之类的框架都定义了构成其MVC层基础的HTTP组件; 甚至像oauth2-server-php这样的小型单用途库也提供并需要自己的HTTP请求/响应实现。Guzzle,Buzz和其他HTTP客户端实现也都创建了自己的HTTP消息实现。
  3. Silex,Stack和Drupal 8等项目对Symfony的HTTP内核有很强的依赖性。任何基于Guzzle的SDK都对Guzzle的HTTP消息实现有严格的要求。
  4. 诸如Geocoder之类的项目为公共库创建冗余适配器

直接使用超全球有许多问题。首先,这些是可变的,这使得库和代码可以改变值,从而改变应用程序的状态。此外,超全球使单元和集成测试变得困难和脆弱,导致代码质量下降。

在当前实现HTTP消息抽象的框架生态系统中,最终结果是项目不具备互操作性或交叉授权。为了使用从另一个框架定位一个框架的代码,第一个业务顺序是在HTTP消息实现之间构建桥接层。在客户端,如果某个特定的库没有可以使用的适配器,如果您希望使用其他库中的适配器,则需要桥接请求/响应对。

最后,当涉及到服务器端响应时,PHP以自己的方式得到:在调用之前发出的任何内容header()都将导致该调用成为无操作; 根据错误报告设置,这通常意味着未正确发送标头和/或响应状态。解决此问题的一种方法是使用PHP的输出缓冲功能,但输出缓冲区的嵌套可能会成为问题并且难以调试。因此,框架和应用程序倾向于为聚合标题和可以立即发出的内容创建响应抽象 - 这些抽象通常是不兼容的。

因此,该提议的目标是抽象客户端和服务器端请求和响应接口,以促进项目之间的互操作性。如果项目实现这些接口,则在采用来自不同库的代码时可能会假定合理的兼容性级别。

应该注意的是,该提议的目标不是废弃现有PHP库使用的当前接口。该提议旨在实现PHP包之间的互操作性,以便描述HTTP消息。

4.范围

4.1目标

  • 提供描述HTTP消息所需的接口。
  • 专注于实际应用和可用性。
  • 定义接口以模拟HTTP消息和URI规范的所有元素。
  • 确保API不对HTTP消息施加任意限制。例如,某些HTTP消息体可能太大而无法存储在内存中,因此我们必须考虑到这一点。
  • 提供有用的抽象,用于处理服务器端应用程序的传入请求和在HTTP客户端中发送传出请求。

4.2非目标

  • 此提议不希望所有HTTP客户端库或服务器端框架更改其接口以符合。它严格意味着互操作性。
  • 虽然每个人对实施细节的内容和内容的看法各不相同,但该提案不应强加实施细节。由于RFC 7230,7231和3986不强制任何特定实现,因此在PHP中描述HTTP消息接口将需要一定量的发明。

5.设计决策

消息设计

MessageInterface常用于所有HTTP消息的元素提供访问器,无论它们是用于请求或响应。这些要素包括:

  • HTTP协议版本(例如,“1.0”,“1.1”)
  • HTTP标头
  • HTTP消息体

更具体的接口用于描述请求和响应,更具体地说,是每个(客户端与服务器端)的上下文。这些部分部分受到现有PHP使用的启发,但也受到其他语言的启发,例如Ruby的Rack,Python的WSGI,Go的http包,Node的http模块等。

为什么消息而不是标题包中有标题方法?

消息本身是标头的容器(以及其他消息属性)。内部如何表示这些是一个实现细节,但对标题的统一访问是消息的责任。

为什么URI表示为对象?

URI是值,由值定义标识,因此应该建模为值对象。

另外,URI包含可以在给定请求中多次访问的各种段 - 并且需要解析URI以便确定(例如,通过parse_url())。将URI建模为值对象只允许解析一次,并简化对各个段的访问。它还通过允许用户仅使用更改的段(例如,仅更新路径)来创建基本URI实例的新实例,从而在客户端应用程序中提供便利。

为什么请求接口有处理请求目标的方法并组成URI?

RFC 7230将请求行详细说明为包含“请求目标”。在四种形式的请求目标中,只有一种是符合RFC 3986的URI; 使用的最常见形式是origin-form,它表示没有方案或权限信息的URI。此外,由于所有表格都适用于请求,因此提案必须适用于每个表格。

RequestInterface因此具有与请求目标有关的方法。默认情况下,它将使用组合的URI来呈现原始形式的请求目标,并且在没有URI实例的情况下,返回字符串“/”。另一种方法 withRequestTarget()允许指定具有特定请求目标的实例,允许用户创建使用其他有效请求目标表单之一的请求。

由于各种原因,URI被保留为请求的离散成员。对于客户端和服务器,通常需要了解绝对URI。在客户端的情况下,需要URI,特别是方案和权限细节,以便进行实际的TCP连接。对于服务器端应用程序,通常需要完整的URI才能验证请求或路由到适当的处理程序。

为何重视物品?

该提案将消息和URI建模为值对象

消息是其中标识是消息的所有部分的聚合的值; 对消息的任何方面的改变本质上是一条新消息。这是值对象的定义。更改导致新实例的实践被称为不可变性,并且是旨在确保给定值的完整性的功能。

该提案还认识到大多数客户端和服务器端应用程序都需要能够轻松更新消息方面,因此,提供了将使用更新创建新消息实例的接口方法。这些通常以措词with词汇为前缀without

在建模HTTP消息时,Value对象提供了几个好处:

  • URI状态的更改不能更改组成URI实例的请求。
  • 标题的更改不能改变组成它们的消息。

本质上,将HTTP消息建模为值对象可确保消息状态的完整性,并防止对双向依赖性的需求,这种依赖性通常会失去同步或导致调试或性能问题。

对于HTTP客户端,它们允许使用者使用诸如基本URI和所需标头之类的数据构建基本请求,而无需为客户端发送的每条消息构建全新请求或重置请求状态:

$uri = new Uri('http://api.example.com');
$baseRequest = new Request($uri, null, [
    'Authorization' => 'Bearer ' . $token,
    'Accept'        => 'application/json',
]);;

$request = $baseRequest->withUri($uri->withPath('/user'))->withMethod('GET');
$response = $client->send($request);

// get user id from $response

$body = new StringStream(json_encode(['tasks' => [
    'Code',
    'Coffee',
]]));;
$request = $baseRequest
    ->withUri($uri->withPath('/tasks/user/' . $userId))
    ->withMethod('POST')
    ->withHeader('Content-Type', 'application/json')
    ->withBody($body);
$response = $client->send($request)

// No need to overwrite headers or body!
$request = $baseRequest->withUri($uri->withPath('/tasks'))->withMethod('GET');
$response = $client->send($request);

在服务器端,开发人员需要:

  • 反序列化请求消息正文。
  • 解密HTTP cookie。
  • 写回复。

这些操作也可以使用值对象来完成,具有许多好处:

  • 可以存储原始请求状态以供任何消费者检索。
  • 可以使用默认标头和/或邮件正文创建默认响应状态。

目前,大多数流行的PHP框架都具有完全可变的HTTP消息。消耗真值对象所需的主要变化是:

  • 不是调用setter方法或设置公共属性,而是调用mutator方法,并分配结果。
  • 开发人员必须在状态更改时通知应用程序。

例如,在Zend Framework 2中,而不是以下内容:

function (MvcEvent $e)
{
    $response = $e->getResponse();
    $response->setHeaderLine('x-foo', 'bar');
}

现在可以写:

function (MvcEvent $e)
{
    $response = $e->getResponse();
    $e->setResponse(
        $response->withHeader('x-foo', 'bar')
    );
}

以上结合了一次通话中的分配和通知。

这种做法的另一个好处是可以明确对应用程序状态进行任何更改。

新实例vs返回$ this

对各种with*()方法的一个观察是,return $this;如果所提出的论点不会导致价值的变化,它们可能是安全的。这样做的一个基本原理是性能(因为这不会导致克隆操作)。

各种接口都是用verbiage编写的,表明必须保留不变性,但只表示必须返回包含新状态的“实例”。由于表示相同值的实例被认为$this是相等的,因此返回在功能上是等效的,因此允许。

使用流而不是X.

MessageInterface使用必须实现的正文值StreamInterface做出这样的设计决定,以便开发人员可以发送和接收(和/或接收和发送)包含比实际存储在内存中的数据更多的数据的HTTP消息,同时仍然允许与消息体作为字符串交互的便利性。虽然PHP通过流包装器提供流抽象,但流资源可能很麻烦:流资源只能使用stream_get_contents()或手动读取字符串的其余部分来转换为字符串。在消耗或填充流时向流添加自定义行为需要注册流过滤器; 但是,只有在使用PHP注册过滤器后才能将流过滤器添加到流中(即,没有流过滤器自动加载机制)。

使用定义良好的流接口允许灵活的流装饰器的潜力,可以将其添加到飞行前的请求或响应中,以启用加密,压缩等功能,确保下载的字节数反映报告的字节数在 JavaNode 社区中,Content-Length装饰流是一种成熟的 模式,允许非常灵活的流。

大多数StreamInterfaceAPI都基于 Python的io模块,该模块提供实用且可消耗的API。相反,实施使用类似流能力WritableStreamInterfaceReadableStreamInterface,用同样的方法来提供流的能力isReadable()isWritable()等等。这种方法是通过Python中,使用 C#,C ++Ruby的节点,并有可能别人。

如果我只想返回一个文件怎么办?

在某些情况下,您可能希望从文件系统返回文件。在PHP中执行此操作的典型方法是以下之一:

readfile($filename);

stream_copy_to_stream(fopen($filename, 'r'), fopen('php://output', 'w'));

注意,上面省略了发送适当的Content-TypeContent-Length标题; 在调用上面的代码之前,开发人员需要发出这些代码。

使用HTTP消息的等效方法是使用StreamInterface 接受文件名和/或流资源的实现,并将其提供给响应实例。一个完整的示例,包括设置适当的标题:

// where Stream is a concrete StreamInterface:
$stream   = new Stream($filename);
$finfo    = new finfo(FILEINFO_MIME);
$response = $response
    ->withHeader('Content-Type', $finfo->file($filename))
    ->withHeader('Content-Length', (string) filesize($filename))
    ->withBody($stream);

发出此响应将文件发送到客户端。

如果我想直接发出输出怎么办?

直接发射输出(例如,通过echoprintf或写入 php://output流)通常仅作为性能优化或发射大数据集时是可取的。如果需要完成并且您仍希望使用HTTP消息范例,则根据此示例,一种方法是使用基于回调的StreamInterface实现在回调中直接包装任何发出输出的代码,将其传递给适当的 实现,并将其提供给消息体:StreamInterface

$output = new CallbackStream(function () use ($request) {
    printf("The requested URI was: %s<br>\n", $request->getUri());
    return '';
});
return (new Response())
    ->withHeader('Content-Type', 'text/html')
    ->withBody($output);

如果我想为内容使用迭代器怎么办?

Ruby的Rack实现使用基于迭代器的方法来处理服务器端响应消息体。这可以通过迭代器支持的StreamInterface方法使用HTTP消息范例进行模拟详见psr7examples存储库

为什么流可变?

StreamInterfaceAPI包括的方法,例如write(),可以更改消息内容-这直接违背具有不可变的消息。

出现的问题是由于接口旨在包装PHP流或类似的事实。因此,写操作将代理写入流。即使我们做了StreamInterface不可变的,一旦流更新了,包装该流的任何实例也将被更新 - 使得不可变性无法实施。

我们的建议是实现使用只读流来处理服务器端请求和客户端响应。

ServerRequestInterface的基本原理

RequestInterfaceResponseInterface具有基本上1:与在所述的请求和响应消息1相关 RFC 7230它们提供了用于实现与其建模的特定HTTP消息类型相对应的值对象的接口。

对于服务器端应用程序,传入请求还有其他注意事项:

  • 访问服务器参数(可能来自请求,但也可能是服务器配置的结果,通常通过$_SERVER超全局表示;这些是PHP Server API(SAPI)的一部分)。
  • 访问查询字符串参数(通常通过$_GET超全局封装在PHP中 )。
  • 访问已解析的主体(即,从传入的请求主体反序列化的数据;在PHP中,这通常是使用application/x-www-form-urlencoded内容类型的POST请求的结果 ,并封装在 $_POST超全局中,但对于非POST,非表单编码的数据,可以是数组或对象)。
  • 访问上传​​的文件(通过$_FILES超全局封装在PHP中)。
  • 访问cookie值(通过$_COOKIE超全局封装在PHP中)。
  • 访问从请求派生的属性(通常但不限于与URL路径匹配的属性)。

对这些参数的统一访问增加了框架和库之间互操作性的可行性,因为他们现在可以假设,如果请求实现ServerRequestInterface,他们可以获得这些值。它还解决了PHP语言本身的问题:

  • 在5.6.0之前,php://input被读过一次; 因此,从多个框架/库实例化多个请求实例可能导致不一致的状态,因为第一个访问php://input将是唯一接收数据的状态。
  • 针对超全局单元测试(例如,$_GET$_FILES等)是困难的并且通常发脆。将它们封装在 ServerRequestInterface实现中可以简化测试注意事项。

为什么在ServerRequestInterface中“解析主体”?

使用术语“BodyParams”进行了论证,并要求该值为数组,具有以下基本原理:

  • 与其他服务器端参数访问的一致性。
  • $_POST 是一个数组,80%的用例将针对超全局。
  • 单一类型可以实现强大的合同,简化使用。

主要论点是,如果body参数是一个数组,开发人员可以预测访问值:

$foo = isset($request->getBodyParams()['foo'])
    ? $request->getBodyParams()['foo']
    : null;

使用“解析体”的论据是通过检查域来完成的。消息正文可以包含任何内容。虽然传统的Web应用程序使用表单并使用POST提交数据,但这是一个快速受当前Web开发趋势挑战的用例,这些趋势通常以API为中心,因此也使用备用请求方法(特别是PUT和PATCH)作为非形式编码的内容(通常JSON或XML),其可以在许多情况下被强制转换为阵列,但在很多情况下也不能不应该

如果强制表示已解析主体的属性只是一个数组,那么开发人员需要一个关于在何处放置解析主体结果的共享约定。这些可能包括:

  • 身体参数下的特殊键,如__parsed__
  • 一个特别命名的属性,例如__body__

最终结果是开发人员现在必须查看多个位置:

$data = $request->getBodyParams();
if (isset($data['__parsed__']) && is_object($data['__parsed__'])) {
    $data = $data['__parsed__'];
}

// or:
$data = $request->getBodyParams();
if ($request->hasAttribute('__body__')) {
    $data = $request->getAttribute('__body__');
}

提出的解决方案是使用术语“ParsedBody”,这意味着值是解析消息体的结果。这也意味着返回值是模糊的; 但是,因为这是域的属性,所以这也是预期的。因此,使用将变为:

$data = $request->getParsedBody();
if (! $data instanceof \stdClass) {
    // raise an exception!
}
// otherwise, we have what we expected

这种方法消除了强制数组的限制,但代价是返回值的模糊性。考虑到其他建议的解决方案 - 将解析后的数据推送到特殊的主体参数密钥或属性中 - 也存在歧义,所提出的解决方案更简单,因为它不需要添加到接口规范中。最终,模糊性使得在表示解析身体的结果时需要灵活性。

为什么没有包含用于检索“基本路径”的功能?

许多框架提供了获取“基本路径”的能力,通常被认为是前端控制器的路径。例如,如果提供应用程序http://example.com/b2b/index.php,并且用于请求它的当前URI是http://example.com/b2b/index.php/customer/register,则将返回检索基本路径的功能/b2b/index.php然后,路由器可以使用此值在尝试匹配之前剥离该路径段。

此值通常也用于应用程序中的URI生成; 参数将传递给路由器,路由器将生成路径,并将其作为基本路径的前缀,以便返回完全限定的URI。其他工具(通常是视图帮助程序,模板过滤器或模板函数)用于解析相对于基本路径的路径,以便生成用于链接到静态资产等资源的URI。

在检查几种不同的实现时,我们注意到以下内容:

  • 用于确定基本路径的逻辑在实现之间变化很大。例如,将ZF2 中的逻辑与Symfony 2中逻辑进行比较
  • 大多数实现似乎允许手动注入路由器的基本路径和/或用于生成URI的任何工具。
  • 主要用例 - 路由和URI生成 - 通常是该功能的唯一消费者; 开发人员通常不需要了解基本路径概念,因为其他对象会处理它们的详细信息。例如:
    • 路由器将在路由期间为您剥离基本路径; 您不需要将修改后的路径传递给路由器。
    • 视图助手,模板过滤器等通常在调用之前注入基本路径。有时这是手动完成的,但更常见的是它是框架连接的结果。
  • 计算基本路径所需的所有源都已经在 RequestInterface实例中,通过服务器参数和URI实例。

我们的立场是基本路径检测是框架和/或应用程序特定的,并且检测结果可以容易地注入到需要它的对象中,和/或根据需要使用RequestInterface实例函数和/或来自实例本身的类来计算

为什么getUploadedFiles()返回对象而不是数组?

getUploadedFiles()返回Psr\Http\Message\UploadedFileInterface 实例这主要是为了简化规范:我们指定一个接口,而不是要求数组的实现规范段落。

此外,a中的数据UploadedFileInterface被规范化以在SAPI和非SAPI环境中工作。这允许创建进程以手动解析消息体并在不首先写入文件系统的情况下将内容分配给流,同时仍允许在SAPI环境中正确处理文件上载。

“特殊”标题值怎么样?

许多标题值包含唯一的表示要求,这些要求可能会导致消费和生成问题; 特别是,cookie和Accept标题。

该提案不对任何标题类型提供任何特殊处理。基础MessageInterface提供了标头检索和设置的方法,所有标头值最后都是字符串值。

鼓励开发人员编写商品库以与这些标头值进行交互,以用于解析或生成。然后,当需要与这些值交互时,用户可以使用这些库。这种做法的例子已经存在于诸如willdurand / NegotiationAura.Accept等库中 只要该对象具有将值转换为字符串的功能,就可以使用这些对象来填充HTTP消息的标头。

6.人

6.1编辑

  • Matthew Weier O'Phinney

6.2赞助商

  • 保罗·琼斯
  • Beau Simensen(协调员)

6.3贡献者

  • 迈克尔道林
  • 拉里加菲尔德
  • 外翻锅
  • Tobias Schultze
  • 伯恩哈德施塞克
  • Anton Serdyuk
  • 菲尔斯特金
  • 克里斯威尔金森