jasny/http-message

此包已被废弃,不再维护。未建议替代包。

处理 HTTP 请求的 PSR-7 实现


README

Build Status Scrutinizer Code Quality Code Coverage SensioLabsInsight Packagist Stable Version Packagist License

此库提供了对PHP各种超级全局变量的抽象,并控制HTTP响应。这种做法有助于减少消费者对超级全局变量的耦合,并鼓励和促进测试请求消费者。

此库仅实现处理接收到的HTTP请求的PSR-7接口。如果您想向其他Web服务发送HTTP请求,我建议使用Guzzle

为什么是这个库?

Jasny HTTP Message是一个简洁的实现,可以与任何框架或库一起使用。

该库的重点是按预期行为,没有不希望和意外的副作用。一个好的例子是解析体的实现。

使用库的基本形式保持尽可能简单。除非您需要自定义,否则您只需处理所有可用类的一个子集。

在使用PSR-7时,不允许直接使用echoheader()输出。相反,您需要使用Response对象。使用超级全局变量如$_GET$_POST也不会起作用,相反,您需要使用ServerRequest对象。

如果您、您的团队或您的项目尚未准备好进行这种范式转变,此库允许您轻松地使用PSR-7。它可以作为正常输入/输出方法和变量(如echoheader()$_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 RequestInterfacePSR-7 ServerRequestInterface

要使用$_SERVER$_COOKIE$_GET$_POST$_FILES超级全局变量以及以php://input作为输入流创建ServerRequest对象,请使用withGlobalEnvironment()方法。

$request = (new Jasny\HttpMessage\ServerRequest())->withGlobalEnvironment();

绑定到全局环境

通过使用withGlobalEnvironment(true)ServerRequest对象通过引用链接超级全局变量。如果您修改这些变量,更改将反映在ServerRequest对象中。反之,使用withQueryParams()将更改$_GETwithServerParams更改$_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-dataapplication/x-www-form-urlencoded,则使用请求数据。

如果请求具有主体内容,并且内容类型为application/jsonapplication/xmltext/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对象仅支持httphttps协议。

$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://tempphp://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-ForClient-IpForwarded发送的地址将予以考虑。

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规范的代码。它可能直接访问超全局变量,并使用echoheaders()进行输出。

// 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服务器请求。