jasny / http-message
处理 HTTP 请求的 PSR-7 实现
Requires
- php: >=5.6.0
- jasny/php-functions: ^2.2|^3.0|^4.0
- psr/http-message: ^1.0
Requires (Dev)
- jasny/php-code-quality: 2.1.*
- mikey179/vfsstream: ^1.6
Provides
- psr/http-message-implementation: ~1.0.0
- dev-master
- v2.x-dev
- v1.3.8
- v1.3.7
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.0
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-integration-tests
- dev-force-stale
- dev-reparse-on-null
- dev-global-environment-interface
- dev-scrutinizer-patch-2
- dev-fix-ungroup-uploaded-files-uri
- dev-fix-revive-response
- dev-parsedbody-charset
- dev-global-env-fix
- dev-interface-naming
- dev-stream-eof
- dev-emit
- dev-headers-fix
- dev-response
- dev-globalstream
- dev-scrutinizer-patch-1
This package is auto-updated.
Last update: 2022-06-26 13:47:08 UTC
README
此库提供了对PHP各种超级全局变量的抽象,并控制HTTP响应。这种做法有助于减少消费者对超级全局变量的耦合,并鼓励和促进测试请求消费者。
此库仅实现处理接收到的HTTP请求的PSR-7接口。如果您想向其他Web服务发送HTTP请求,我建议使用Guzzle。
为什么是这个库?
Jasny HTTP Message是一个简洁的实现,可以与任何框架或库一起使用。
该库的重点是按预期行为,没有不希望和意外的副作用。一个好的例子是解析体的实现。
使用库的基本形式保持尽可能简单。除非您需要自定义,否则您只需处理所有可用类的一个子集。
在使用PSR-7时,不允许直接使用echo
和header()
输出。相反,您需要使用Response
对象。使用超级全局变量如$_GET
和$_POST
也不会起作用,相反,您需要使用ServerRequest
对象。
如果您、您的团队或您的项目尚未准备好进行这种范式转变,此库允许您轻松地使用PSR-7。它可以作为正常输入/输出方法和变量(如echo
、header()
、$_GET
、$_POST
等)的抽象层。
安装
composer require jasny/http-message
文档
该库实现了以下PSR-7接口
ServerRequest
实现Psr\Http\Message\ServerRequestInterface
Response
实现Psr\Http\Message\ResponseInterface
Stream
实现Psr\Http\Message\StreamInterface
Uri
实现Psr\Http\Message\UriInterface
它定义了一个接口
ServerRequest
ServerRequest
类表示Web服务器接收并经过PHP处理的HTTP请求。
有关ServerRequest
的完整文档,请参阅PSR-7 RequestInterface
和PSR-7 ServerRequestInterface
。
要使用$_SERVER
、$_COOKIE
、$_GET
、$_POST
和$_FILES
超级全局变量以及以php://input
作为输入流创建ServerRequest
对象,请使用withGlobalEnvironment()
方法。
$request = (new Jasny\HttpMessage\ServerRequest())->withGlobalEnvironment();
绑定到全局环境
通过使用withGlobalEnvironment(true)
,ServerRequest
对象通过引用链接超级全局变量。如果您修改这些变量,更改将反映在ServerRequest
对象中。反之,使用withQueryParams()
将更改$_GET
,withServerParams
更改$_SERVER
等。
use Jasny\HttpMessage\ServerRequest; // $_GET is not affected $requestByVal = (new ServerRequest())->withGlobalEnvironment(); $requestByVal = $request->withQueryParams(['foo' => 1]); var_dump($_GET); // array(0) { } // $_GET is affected $requestByRef = (new ServerRequest())->withGlobalEnvironment(true); $requestByRef = $request->withQueryParams(['foo' => 1]); var_dump($_GET); // array(1) { ["foo"]=> int(1) }
解析后的主体
getParsedBody()
方法可以执行多项操作。
如果显式调用了withParsedBody($data)
,则提供的数据将始终返回,无论头部或其他请求属性如何。
如果从全局环境复制了$_POST
,并且内容类型为multipart/form-data
或application/x-www-form-urlencoded
,则使用请求数据。
如果请求具有主体内容,并且内容类型为application/json
、application/xml
或text/xml
,则解析主体内容。对于XML,这将产生一个SimpleXmlElement
。
如果$_POST
没有复制,主体还会解析为application/x-www-form-urlencoded
。然而,multipart/form-data
永远不会手动解析,因此在这种情况下,如果没有复制$_POST
,则抛出异常。
如果内容类型未知,getParsedBody()
将简单地返回null。如果主体有内容,但没有设置内容类型头部,则触发警告。
如果头部或主体内容更改,则在调用getParsedBody()
时将重新解析主体。然而,这只会在没有使用withParsedBody()
显式设置解析后的主体时发生。
响应
Response
类允许您创建出站的HTTP响应。
有关Response
类的完整文档,请参阅PSR-7 ResponseInterface
。
默认情况下,Response
对象将流到php://temp
并简单地保留所有设置的头部列表。
$response = new Jasny\HttpMessage\Response();
发出
响应对象包含所有输出,包括头部和主体内容。要将其发送到客户端(换句话说,输出它),请使用emit()
方法。
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\Response; $request = (new ServerRequest())->withGlobalEnvironment(); $response = $router->handle($request, new Response()); $response->emit();
emit()
方法将创建一个Emitter
对象。如果需要,您可以创建自己的类,该类实现EmitterInterface
,并将其作为$response->emit(new CustomEmitter())
传递。
也可以直接使用发射器而不使用响应的emit()
方法。这也很有用,如果您不确定路由器/中间件/控制器是否会返回一个Jasny/HttpMessage/Response
或可能返回其他PSR-7 ResponseInterface
实现。
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\Response; use Jasny\HttpMessage\Emitter; $request = (new ServerRequest())->withGlobalEnvironment(); $response = $router->handle($request, new Response()); $emitter = new Emitter(); $emitter->emit($response);
绑定到全局环境
要创建使用header()
方法和以php://output
作为输出流的Response
对象,请使用withGlobalEnvironment(true)
方法。
$request = (new Response())->withGlobalEnvironment(true); $request->withHeader('Content-Type', 'text/plain'); // Does `header("Content-Type: text/plain")` $request->getBody()->write('hello world'); // Outputs "hello world"
URI
Uri
类旨在根据RFC 3986表示URI。它允许您获取和更改uri的任何特定部分。
有关Uri
类的完整文档,请参阅PSR-7 UriInterface
。
创建Uri时,您可以传递作为字符串的URL,或者以关联数组的部分形式传递URL。有关URL部分,请参阅parse_url
函数。
Jasny\HttpMessage\Uri
对象仅支持http
和https
协议。
$uri = new Jasny\HttpMessage\Uri("http://www.example.com/foo");
流
Stream
类是对php流的封装,实现了PSR-7 StreamInterface
。
$input = new Jasny\HttpMessage\Stream(); $input->write(json_encode(['foo' => 'bar', 'color' => 'red']));
创建流
默认情况下,它将创建一个使用php://temp
的流。在创建流时,您可以传递流资源以使用不同类型的处理。
$handle = fopen('php://memory', 'r+'); $stream = new Jasny\HttpMessage\Stream($handle);
或者,您可以使用Stream::open($uri, $mode)
创建具有特定处理的流。
$stream = Jasny\HttpMessage\Stream::open('php://memory', 'r+');
克隆流
在克隆流时,处理程序将被重新创建。这意味着对于php://temp
和php://memory
,您将得到一个没有任何内容的流。清除响应体的内容通常可以通过克隆流来实现。
$newResponse = $response->withBody(clone $response->getBody());
此行为未在PSR-7中指定,并且克隆流可能与其他PSR-7实现不兼容。
派生属性
您可以使用withAttribute()
方法为ServerRequest
设置任意属性。要获取属性,请使用getAttribute()
方法。
属性可以设置为任何静态值,也可以从ServerRequest
对象的其它值派生,例如标题或查询参数。创建派生属性的最简单方法是使用一个Closure
。
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withAttribute('accept_json', function(ServerRequest $request) { $accept = $request->getHeaderLine('Accept'); return strpos($accept, 'application/json') !== false || strpos($accept, '*/*') !== false; });
您可以通过创建一个实现DerivedAttributeInterface
接口的类来创建更复杂的派生属性。实现该接口时,实现__invoke(ServerRequest $request)
。
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttributeInterface; class DetectBot implements DerivedAttributeInterface { public static $identifiers = [ 'google' => 'googlebot', 'yahoo' => 'yahoobot', 'magpie' => 'magpie-crawler' ]; protected $detect = []; public function __construct(array $detect) { $this->detect = $detect; } public function __invoke(ServerRequest $request) { $useragent = $request->getHeaderLine('User-Agent'); $detected = false; foreach ($this->detect as $bot) { $identifier = static::$identifiers[$bot]; $detected = $detected || stripos($useragent, $bot) !== false; } return $detected; } } $request = (new ServerRequest()) ->withAttribute('is_friendly_bot', new DetectBot(['google', 'yahoo'])) ->withAttribute('is_annoying_bot', new DetectBot(['magpie']));
请记住,ServerRequest
方法是不可变的,所以withAttribute()
将创建一个新的对象。
此库附带了一些派生属性,可以用于。
客户端IP
获取客户端IP。默认情况下,只返回$_SERVER['REMOTE_ADDR']
。
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withGlobalEnvironment(); $request->getAttribute('client_ip'); // always returns $_SERVER['REMOTE_ADDR']
您可以为受信任的代理指定IP或CIDR地址。当使用时,通过X-Forwarded-For
、Client-Ip
或Forwarded
发送的地址将予以考虑。
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttribute\ClientIp; $request = (new ServerRequest()) ->withGlobalEnvironment() ->withAttribute('client_ip', new ClientIp(['trusted_proxy => '10.0.0.0/24']); $ip = $request->getAttribute('client_ip'); // for a request from the internal network, use the `X-Forwarded-For` header
注意:如果设置了这些头中的多个,将抛出RuntimeException
。这可以防止用户注入一个Client-Ip
地址来伪造其IP,而您的代理正在设置X-Forwarded-For
头。为了确保不发生此异常,请删除所有意外的转发头。
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttribute\ClientIp; $request = (new ServerRequest()) ->withGlobalEnvironment() ->withoutHeader('Client-Ip') ->withoutHeader('Forwarded') ->withAttribute('client_ip', new ClientIp(['trusted_proxy' => '10.0.0.0/24']);
IsXhr
测试是否使用AJAX发起的请求。
所有现代浏览器在发起AJAX请求时都将X-Requested-With
头设置为XMLHttpRequest
。此派生属性简单地检查该头。
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withGlobalEnvironment(); $isXhr = $request->getAttribute('is_xhr'); // true or false
LocalReferer
返回Referer
头的路径,但只有当引用的方案、主机和端口与请求的方案、主机和端口匹配时。
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withGlobalEnvironment(); $back = $request->getAttribute('local_referer') ?: '/'; // Referer Uri path, defaults to `/` for no or external referer
如果需要,可以禁用对方案和/或端口的检查。
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttribute\LocalReferer; $request = (new ServerRequest()) ->withGlobalEnvironment() ->withAttribute('local_referer', new LocalReferer(['checkScheme' => false, 'checkPort' => false]));
测试
当测试完全符合PSR-7规范的代码时,创建一个带有特定头部、参数和数据的ServerRequest
和一个默认的Response
。
$request = (new ServerRequest()) ->withMethod('GET') ->withUri('/foo') ->withQueryParams(['page' => 1]);
符合PSR-7规范的代码不得直接访问超全局变量,并且也不得直接输出头部和数据。
测试遗留代码
这个库允许你测试不完全符合PSR-7规范的代码。它可能直接访问超全局变量,并使用echo
和headers()
进行输出。
// Start output buffering, so the output isn't send directly ob_start(); // Create server request that is bound to the global enviroment. $baseRequest = (new ServerRequest())->withGlobalEnvironment(true); // Modifying the bound request, modifies the superglobals. $request = $baseRequest ->withServerParams(['REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/foo']) ->withQueryParams(['page' => 1]); // Create response that is bound to the global enviroment. $baseResponse = (new Response())->withGlobalEnvironment(true); // Some PSR-7 compatible router handles the request. The code uses `header` and `echo` to output. $router->handle($request, $baseResponse); // Disconnect the global environment, copy the data and headers $response = $response->withoutGlobalEnvironment(); // Refiving the base request and response, restores the global environment. Also clean the output buffer. $baseRequest = $baseRequest->revive(); $baseResponse = $baseResponse->revive()->withBody(new OutputBufferStream()); // Assert response ... // Ready for next request :)
陈旧和复活
使用这项技术,你可以在不重写整个代码库的情况下开始使用PSR-7。相反,你可以逐步重构你的代码。
在进行$copy = $object->with..()
时,$copy
现在绑定到全局环境,而$object
已变得陈旧。
陈旧意味着对象绑定到全局环境,但不再反映当前状态。全局环境的状态已复制到对象中(可以想象它是被冻结在时间中的)。全局环境的变化不会影响陈旧的对象。无法修改陈旧的对象。
请注意,Stream
是一个资源,它不会被with...
方法克隆。当Response
绑定到输出流时也是如此。因此,输出确实会影响陈旧的响应对象。
在某些情况下,你可能想继续使用陈旧的对象。例如,在中间件中捕获错误时。在这种情况下,你需要调用revive()
。这个方法将全局环境恢复到陈旧对象的状态。
function errorHandlerMiddleware(ServerRequestInterface $request, ResponseInterface $response, $next) { try { $newResponse = $next($request, $response); } catch (Throwable $error) { // If the next middleware or controller has done something like set the response status, the response is stale. if ($request instanceof Jasny\HttpMessage\ServerRequest) { $request = $request->revive(); } if ($response instanceof Jasny\HttpMessage\Response) { $response = $response->revive(); } $newResponse = handleError($request, $response, $error); } return $newResponse; }
Codeception
如果你使用Codeception,那么Jasny Codeception模块可能很有趣。它使用Jasny Router来处理PSR-7服务器请求。