PSR
PSR-7、PSR-17和PSR-18的引入都是计划的一部分,以使
构建需要以与 HTTP 客户端无关的方式向服务器发送 HTTP 请求的应用程序
我一直在使用许多过去严重依赖 Guzzle 而不是抽象接口的应用程序。这些应用程序中的大多数使用包含 JSON 正文的 GET 或 POST 请求发出简单的 API 请求,响应也包含 JSON 正文或抛出 HTTP 4xx 或 5xx 错误的异常。
API 包装器
这个问题来自最近的一个项目,我尝试开发一个 API 包,该包没有明确依赖 Guzzle,而是仅依赖于 PSR 接口。
这个想法是创建一个ApiWrapper
可以使用以下方法启动的类:
- 满足 PSR-18的HTTP 客户端
ClientInterface
- 满足 PSR-17的请求工厂
RequestFactoryInterface
- 满足 PSR-17的流工厂
StreamFactoryInterface
这个类将有它需要的任何东西:
- 使用请求工厂和流工厂发出请求 (PSR-7)
- 使用HTTP 客户端发送请求
- 处理响应 - 因为我们知道这将满足 PSR-7
ResponseInterface
这样的 API 包装器不依赖于上述接口的任何具体实现,而只需要这些接口的任何实现。因此,开发人员将能够使用他或她最喜欢的 HTTP 客户端,而不是被迫使用像 Guzzle 这样的特定客户端。
问题
现在,首先,我真的很喜欢 Guzzle,这不是一篇文章来质疑 Guzzle 的厉害之处,这只是一篇询问如何让开发人员能够根据自己的需要选择正确的 http 客户端的文章。
但问题是显式依赖 Guzzle 提供了很多不错的功能,因为 Guzzle 所做的比上述更多。Guzzle 还应用了一系列处理程序和中间件,例如跟踪重定向或为 HTTP 4xx 响应引发异常。
问题
描述很长,但问题来了:如何处理常见的 HTTP 请求处理,例如以受控方式跟踪重定向或抛出 HTTP 4xx 响应的异常(因此无论使用何种 HTTP 客户端都会产生相同的响应),而无需准确指定使用什么 HTTP 客户端?
例子
下面是一个ApiWrapper
实现示例:
<?php
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
/*
* API Wrapper using PSR-18 ClientInterface, PSR-17 RequestFactoryInterface and PSR-7 RequestInterface
*
* Inspired from: https://www.php-fig.org/blog/2018/11/psr-18-the-php-standard-for-http-clients/
* Require the packages `psr/http-client` and `psr/http-factory`
*
* Details about PSR-7 taken from https://www.dotkernel.com/dotkernel3/what-is-psr-7-and-how-to-use-it/
*
* Class Name Description
* Psr\Http\Message\MessageInterface Representation of a HTTP message
* Psr\Http\Message\RequestInterface Representation of an outgoing, client-side request.
* Psr\Http\Message\ServerRequestInterface Representation of an incoming, server-side HTTP request.
* Psr\Http\Message\ResponseInterface Representation of an outgoing, server-side response.
* Psr\Http\Message\StreamInterface Describes a data stream
* Psr\Http\Message\UriInterface Value object representing a URI.
* Psr\Http\Message\UploadedFileInterface Value object representing a file uploaded through an HTTP request.
*/
class ApiWrapper
{
/**
* The PSR-18 compliant ClientInterface.
*
* @var ClientInterface
*/
private $psr18HttpClient;
/**
* The PSR-17 compliant RequestFactoryInterface.
*
* @var RequestFactoryInterface
*/
private $psr17HttpRequestFactory;
/**
* The PSR-17 compliant StreamFactoryInterface.
*
* @var StreamFactoryInterface
*/
private $psr17HttpStreamFactory;
public function __construct(
ClientInterface $psr18HttpClient,
RequestFactoryInterface $psr17HttpRequestFactory,
StreamFactoryInterface $psr17HttpStreamFactory,
array $options = []
) {
$this->psr18HttpClient($psr18HttpClient);
$this->setPsr17HttpRequestFactory($psr17HttpRequestFactory);
$this->setPsr17HttpStreamFactory($psr17HttpStreamFactory);
}
public function psr18HttpClient(ClientInterface $psr18HttpClient): void
{
$this->psr18HttpClient = $psr18HttpClient;
}
public function setPsr17HttpRequestFactory(RequestFactoryInterface $psr17HttpRequestFactory): void
{
$this->psr17HttpRequestFactory = $psr17HttpRequestFactory;
}
public function setPsr17HttpStreamFactory(StreamFactoryInterface $psr17HttpStreamFactory): void
{
$this->psr17HttpStreamFactory = $psr17HttpStreamFactory;
}
public function makeRequest(string $method, $uri, ?array $headers = [], ?string $body = null): RequestInterface
{
$request = $this->psr17HttpRequestFactory->createRequest($method, $uri);
if (! empty($headers)) {
$request = $this->addHeadersToRequest($request, $headers);
}
if (! empty($body)) {
$stream = $this->createStreamFromString($body);
$request = $this->addStreamToRequest($request, $stream);
}
return $request;
}
/**
* Add headers provided as nested array.
*
* Format of headers:
* [
* 'accept' => [
* 'text/html',
* 'application/xhtml+xml',
* ],
* ]
* results in the header: accept:text/html, application/xhtml+xml
* See more details here: https://www.php-fig.org/psr/psr-7/#headers-with-multiple-values
*
* @param \Psr\Http\Message\RequestInterface $request
* @param array $headers
* @return \Psr\Http\Message\RequestInterface
*/
public function addHeadersToRequest(RequestInterface $request, array $headers): RequestInterface
{
foreach ($headers as $headerKey => $headerValue) {
if (is_array($headerValue)) {
foreach ($headerValue as $key => $value) {
if ($key == 0) {
$request->withHeader($headerKey, $value);
} else {
$request->withAddedHeader($headerKey, $value);
}
}
} else {
$request->withHeader($headerKey, $headerValue);
}
}
return $request;
}
/**
* Use the PSR-7 complient StreamFactory to create a stream from a simple string.
*
* @param string $body
* @return \Psr\Http\Message\StreamInterface
*/
public function createStreamFromString(string $body): StreamInterface
{
return $this->psr17HttpStreamFactory->createStream($body);
}
/**
* Add a PSR 7 Stream to a PSR 7 Request.
*
* @param \Psr\Http\Message\RequestInterface $request
* @param \Psr\Http\Message\StreamInterface $body
* @return \Psr\Http\Message\RequestInterface
*/
public function addStreamToRequest(RequestInterface $request, StreamInterface $body): RequestInterface
{
return $request->withBody($body);
}
/**
* Make the actual HTTP request.
*
* @param \Psr\Http\Message\RequestInterface $request
* @return \Psr\Http\Message\ResponseInterface
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function request(RequestInterface $request): ResponseInterface
{
// According to PSR-18:
// A Client MUST throw an instance of Psr\Http\Client\ClientExceptionInterface
// if and only if it is unable to send the HTTP request at all or if the
// HTTP response could not be parsed into a PSR-7 response object.
return $this->psr18HttpClient->sendRequest($request);
}
}