此提议的目的是为RFC 7230和 RFC 7231中描述的HTTP消息提供一组公共接口,并在RFC 3986(在HTTP消息的上下文中)中描述URI 。
所有HTTP消息都包含正在使用的HTTP协议版本,标头和消息正文。甲请求建立在消息,以包括用于使请求的HTTP方法,以及其中所述请求是由所述URI。甲 响应包括HTTP状态代码和原因短语。
在PHP中,HTTP消息在两个上下文中使用:
ext/curl
扩展,PHP的本地流层等发送HTTP请求,并处理收到的HTTP响应。换句话说,当使用PHP作为HTTP客户端时,使用HTTP消息。该提议提供了一个API,用于完整描述PHP中各种HTTP消息的所有部分。
PHP没有内置的HTTP消息支持。
PHP支持通过以下几种机制发送HTTP请求:
PHP流是发送HTTP请求的最方便和最普遍的方式,但在正确配置SSL支持方面存在许多限制,并且在设置诸如头之类的东西时提供了麻烦的界面。cURL提供了完整且扩展的功能集,但由于它不是默认扩展名,因此通常不存在。http扩展程序遇到与cURL相同的问题,以及它传统上使用的示例少得多。
大多数现代HTTP客户端库倾向于抽象实现,以确保它们可以在任何上面执行的任何环境中工作。
PHP使用服务器API(SAPI)来解释传入的HTTP请求,编组输入以及将处理传递给脚本。最初的SAPI设计镜像了通用网关接口,它可以在将委托传递给脚本之前封送请求数据并将其推送到环境变量中; 然后,脚本将从环境变量中提取,以处理请求并返回响应。
PHP的SAPI设计通过superglobals(,和,分别)提取常见的输入源,如cookie,查询字符串参数和url编码的POST内容$_COOKIE
,为Web开发人员提供了一层便利。$_GET
$_POST
在等式的响应方面,PHP最初是作为模板语言开发的,并允许混合HTML和PHP; 文件的任何HTML部分都会立即刷新到输出缓冲区。现代应用程序和框架避开了这种做法,因为它可能导致发出状态行和/或响应头的问题; 它们倾向于聚合所有标题和内容,并在所有其他应用程序处理完成时立即发出它们。需要特别注意确保错误报告和将内容发送到输出缓冲区的其他操作不会刷新输出缓冲区。
HTTP消息用于许多PHP项目 - 客户端和服务器。在每种情况下,我们都会观察以下一种或多种模式或情况:
例如:
直接使用超全球有许多问题。首先,这些是可变的,这使得库和代码可以改变值,从而改变应用程序的状态。此外,超全球使单元和集成测试变得困难和脆弱,导致代码质量下降。
在当前实现HTTP消息抽象的框架生态系统中,最终结果是项目不具备互操作性或交叉授权。为了使用从另一个框架定位一个框架的代码,第一个业务顺序是在HTTP消息实现之间构建桥接层。在客户端,如果某个特定的库没有可以使用的适配器,如果您希望使用其他库中的适配器,则需要桥接请求/响应对。
最后,当涉及到服务器端响应时,PHP以自己的方式得到:在调用之前发出的任何内容header()
都将导致该调用成为无操作; 根据错误报告设置,这通常意味着未正确发送标头和/或响应状态。解决此问题的一种方法是使用PHP的输出缓冲功能,但输出缓冲区的嵌套可能会成为问题并且难以调试。因此,框架和应用程序倾向于为聚合标题和可以立即发出的内容创建响应抽象 - 这些抽象通常是不兼容的。
因此,该提议的目标是抽象客户端和服务器端请求和响应接口,以促进项目之间的互操作性。如果项目实现这些接口,则在采用来自不同库的代码时可能会假定合理的兼容性级别。
应该注意的是,该提议的目标不是废弃现有PHP库使用的当前接口。该提议旨在实现PHP包之间的互操作性,以便描述HTTP消息。
在MessageInterface
常用于所有HTTP消息的元素提供访问器,无论它们是用于请求或响应。这些要素包括:
更具体的接口用于描述请求和响应,更具体地说,是每个(客户端与服务器端)的上下文。这些部分部分受到现有PHP使用的启发,但也受到其他语言的启发,例如Ruby的Rack,Python的WSGI,Go的http包,Node的http模块等。
消息本身是标头的容器(以及其他消息属性)。内部如何表示这些是一个实现细节,但对标题的统一访问是消息的责任。
URI是值,由值定义标识,因此应该建模为值对象。
另外,URI包含可以在给定请求中多次访问的各种段 - 并且需要解析URI以便确定(例如,通过parse_url()
)。将URI建模为值对象只允许解析一次,并简化对各个段的访问。它还通过允许用户仅使用更改的段(例如,仅更新路径)来创建基本URI实例的新实例,从而在客户端应用程序中提供便利。
RFC 7230将请求行详细说明为包含“请求目标”。在四种形式的请求目标中,只有一种是符合RFC 3986的URI; 使用的最常见形式是origin-form,它表示没有方案或权限信息的URI。此外,由于所有表格都适用于请求,因此提案必须适用于每个表格。
RequestInterface
因此具有与请求目标有关的方法。默认情况下,它将使用组合的URI来呈现原始形式的请求目标,并且在没有URI实例的情况下,返回字符串“/”。另一种方法
withRequestTarget()
允许指定具有特定请求目标的实例,允许用户创建使用其他有效请求目标表单之一的请求。
由于各种原因,URI被保留为请求的离散成员。对于客户端和服务器,通常需要了解绝对URI。在客户端的情况下,需要URI,特别是方案和权限细节,以便进行实际的TCP连接。对于服务器端应用程序,通常需要完整的URI才能验证请求或路由到适当的处理程序。
该提案将消息和URI建模为值对象。
消息是其中标识是消息的所有部分的聚合的值; 对消息的任何方面的改变本质上是一条新消息。这是值对象的定义。更改导致新实例的实践被称为不可变性,并且是旨在确保给定值的完整性的功能。
该提案还认识到大多数客户端和服务器端应用程序都需要能够轻松更新消息方面,因此,提供了将使用更新创建新消息实例的接口方法。这些通常以措词with
或
词汇为前缀without
。
在建模HTTP消息时,Value对象提供了几个好处:
本质上,将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);
在服务器端,开发人员需要:
这些操作也可以使用值对象来完成,具有许多好处:
目前,大多数流行的PHP框架都具有完全可变的HTTP消息。消耗真值对象所需的主要变化是:
例如,在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')
);
}
以上结合了一次通话中的分配和通知。
这种做法的另一个好处是可以明确对应用程序状态进行任何更改。
对各种with*()
方法的一个观察是,return $this;
如果所提出的论点不会导致价值的变化,它们可能是安全的。这样做的一个基本原理是性能(因为这不会导致克隆操作)。
各种接口都是用verbiage编写的,表明必须保留不变性,但只表示必须返回包含新状态的“实例”。由于表示相同值的实例被认为$this
是相等的,因此返回在功能上是等效的,因此允许。
MessageInterface
使用必须实现的正文值StreamInterface
。做出这样的设计决定,以便开发人员可以发送和接收(和/或接收和发送)包含比实际存储在内存中的数据更多的数据的HTTP消息,同时仍然允许与消息体作为字符串交互的便利性。虽然PHP通过流包装器提供流抽象,但流资源可能很麻烦:流资源只能使用stream_get_contents()
或手动读取字符串的其余部分来转换为字符串。在消耗或填充流时向流添加自定义行为需要注册流过滤器; 但是,只有在使用PHP注册过滤器后才能将流过滤器添加到流中(即,没有流过滤器自动加载机制)。
使用定义良好的流接口允许灵活的流装饰器的潜力,可以将其添加到飞行前的请求或响应中,以启用加密,压缩等功能,确保下载的字节数反映报告的字节数在
Java
和Node
社区中,Content-Length
装饰流是一种成熟的
模式,允许非常灵活的流。
大多数StreamInterface
API都基于
Python的io模块,该模块提供实用且可消耗的API。相反,实施使用类似流能力WritableStreamInterface
和
ReadableStreamInterface
,用同样的方法来提供流的能力isReadable()
,isWritable()
等等。这种方法是通过Python中,使用
C#,C ++,
Ruby的,
节点,并有可能别人。
在某些情况下,您可能希望从文件系统返回文件。在PHP中执行此操作的典型方法是以下之一:
readfile($filename);
stream_copy_to_stream(fopen($filename, 'r'), fopen('php://output', 'w'));
注意,上面省略了发送适当的Content-Type
和
Content-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);
发出此响应将文件发送到客户端。
直接发射输出(例如,通过echo
,printf
或写入
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存储库。
该StreamInterface
API包括的方法,例如write()
,可以更改消息内容-这直接违背具有不可变的消息。
出现的问题是由于接口旨在包装PHP流或类似的事实。因此,写操作将代理写入流。即使我们做了StreamInterface
不可变的,一旦流更新了,包装该流的任何实例也将被更新 - 使得不可变性无法实施。
我们的建议是实现使用只读流来处理服务器端请求和客户端响应。
的RequestInterface
和ResponseInterface
具有基本上1:与在所述的请求和响应消息1相关
RFC 7230。它们提供了用于实现与其建模的特定HTTP消息类型相对应的值对象的接口。
对于服务器端应用程序,传入请求还有其他注意事项:
$_SERVER
超全局表示;这些是PHP Server API(SAPI)的一部分)。$_GET
超全局封装在PHP中
)。application/x-www-form-urlencoded
内容类型的POST请求的结果
,并封装在
$_POST
超全局中,但对于非POST,非表单编码的数据,可以是数组或对象)。$_FILES
超全局封装在PHP中)。$_COOKIE
超全局封装在PHP中)。对这些参数的统一访问增加了框架和库之间互操作性的可行性,因为他们现在可以假设,如果请求实现ServerRequestInterface
,他们可以获得这些值。它还解决了PHP语言本身的问题:
php://input
被读过一次; 因此,从多个框架/库实例化多个请求实例可能导致不一致的状态,因为第一个访问php://input
将是唯一接收数据的状态。$_GET
,$_FILES
等)是困难的并且通常发脆。将它们封装在
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。
在检查几种不同的实现时,我们注意到以下内容:
RequestInterface
实例中,通过服务器参数和URI实例。我们的立场是基本路径检测是框架和/或应用程序特定的,并且检测结果可以容易地注入到需要它的对象中,和/或根据需要使用RequestInterface
实例函数和/或来自实例本身的类来计算。
getUploadedFiles()
返回Psr\Http\Message\UploadedFileInterface
实例树。这主要是为了简化规范:我们指定一个接口,而不是要求数组的实现规范段落。
此外,a中的数据UploadedFileInterface
被规范化以在SAPI和非SAPI环境中工作。这允许创建进程以手动解析消息体并在不首先写入文件系统的情况下将内容分配给流,同时仍允许在SAPI环境中正确处理文件上载。
许多标题值包含唯一的表示要求,这些要求可能会导致消费和生成问题; 特别是,cookie和Accept
标题。
该提案不对任何标题类型提供任何特殊处理。基础MessageInterface
提供了标头检索和设置的方法,所有标头值最后都是字符串值。
鼓励开发人员编写商品库以与这些标头值进行交互,以用于解析或生成。然后,当需要与这些值交互时,用户可以使用这些库。这种做法的例子已经存在于诸如willdurand / Negotiation和 Aura.Accept等库中 。只要该对象具有将值转换为字符串的功能,就可以使用这些对象来填充HTTP消息的标头。