ПСР
Введение PSR-7, PSR-17 и PSR-18 — это часть плана, позволяющего
создавать приложения, которым необходимо отправлять HTTP-запросы на сервер, независимо от HTTP-клиента.
См. PSR- 18. Стандарт PHP для HTTP-клиентов
Я работал со многими приложениями, которые исторически в значительной степени полагались на Guzzle, а не на абстрактные интерфейсы. Большинство этих приложений делают простой запрос API, используя запрос GET или POST, содержащий тело JSON, и ответы, также содержащие тело JSON, или выдают исключения для ошибок HTTP 4xx или 5xx.
API-оболочка
Этот вопрос связан с недавним проектом, в котором я пытался разработать пакет API, который явно не полагался на Guzzle, а только на интерфейсы PSR.
Идея заключалась в том, чтобы создать класс ApiWrapper
, который можно было бы инициировать с помощью:
- HTTP-клиент, соответствующий PSR-18
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);
}
}