Независимая от клиента API-оболочка, использующая PSR 7, 17 и 18 вместо Guzzle

ПСР

Введение 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, который можно было бы инициировать с помощью:

  1. HTTP-клиент, соответствующий PSR-18 ClientInterface
  2. Фабрика запросов, выполняющая PSR-17 RequestFactoryInterface
  3. Фабрика потоков, выполняющая PSR-17 StreamFactoryInterface

Этот класс будет иметь все, что ему нужно:

  1. Создайте запрос (PSR-7) с помощью Фабрики запросов и Фабрики потоков.
  2. Отправить запрос с помощью HTTP-клиента
  3. Обработайте ответ — так как мы знаем, что это выполнит 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);
    }
}

person user3390352    schedule 29.12.2019    source источник


Ответы (1)


Вот мое мнение, в основном основанное на опробовании нескольких подходов.

Любой клиент PSR-18 будет иметь интерфейс, которому он должен соответствовать. Этот интерфейс, по сути, представляет собой всего один метод — sendRequest(). Этот метод отправит запрос PSR-7 и вернет ответ PSR-7.

Большая часть того, что входит в запрос, будет использоваться для построения запроса PSR-7. Это будет собрано до того, как попадет в sendRequest() клиента. Что спецификация PSR-18 не определяет, так это поведение клиента, например, следовать ли перенаправлениям. В нем указано, что исключения не должны создаваться в случае ответа, отличного от 2XX.

Это может показаться очень ограничительным, но этот клиент является концом линии, он просто связан с физической отправкой запроса и захватом ответа. Все остальное, связанное с поведением клиента, может быть встроено в промежуточное ПО для расширения этого клиента.

Так что же может промежуточное ПО PSR-18?

  • Он имеет доступ к исходному запросу PSR-7, поэтому этот запрос можно прочитать и изменить.
  • У него есть доступ к ответу PSR-7, поэтому он может изменять ответ и выполнять действия на основе этого ответа.
  • Он выполняет вызов sendRequest(), поэтому может применять логику в том, как это обрабатывается, например, повторные попытки, последующие перенаправления и так далее.

Спецификация PSR-18 не упоминает промежуточное ПО, так где же оно? Одним из способов реализации этого может быть декоратор. Декоратор оборачивает базовый клиент PSR-18, добавляя функциональность, но будет представлять себя как клиент PSR-18. Это означает, что на базовый клиент можно наложить несколько декораторов, чтобы добавить любое количество функций, которые вам нравятся.

Вот пример декоратора PSR-18. Этот декоратор по сути ничего не делает, но предоставляет основу для размещения логики.

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class Psr18Decorator implements ClientInterface
{
    // ClientInterface

    protected $client;

    // Instantiate with the current PSR-18 client.
    // Options could be added here for configuring the decorator.

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        // The request can be processed here.

        // Send the request, just once in this example.

        $response = $this->client->sendRequest($request);

        // The response can be processed or acted on here.

        return $response;
    }

    // This is added so that if a decorator adds new methods,
    // they can be accessed from the top, multiple layers deep.

    public function __call($method, $parameters)
    {
        $result = $this->client->$method(...$parameters);

        return $result === $this->client ? $this : $result;
    }
}

Итак, учитывая базовый клиент PSR-18, его можно оформить так:

$decoratedPsr18Client = new Psr18Decorator($basePsr18Client);

Каждый декоратор может быть написан для обработки одной проблемы. Вы можете, например, захотеть создать исключение, если ответ не возвращает код 2XX. Для этого можно написать декоратор.

Другой декоратор может обрабатывать токены OAuth или отслеживать доступ к API, чтобы его можно было ограничить по скорости. Другой декоратор может следовать перенаправлениям.

Итак, вам нужно самим писать все эти декораторы? Пока что да, потому что их катастрофически не хватает. Однако, поскольку они разрабатываются и публикуются в виде пакетов, они, по сути, представляют собой повторно используемый код, который можно применять к любому клиенту PSR-18.

Guzzle великолепен, имеет множество функций и в этом отношении является монолитным. Я считаю, что подход PSR-18 должен позволить нам разбить все эти функции на более мелкие автономные части, чтобы их можно было применять по мере необходимости. Пакет управления декоратором может помочь добавить эти декораторы (возможно, обеспечив их правильный порядок и совместимость друг с другом) и, возможно, по-другому обработать пользовательские методы декоратора, чтобы избежать необходимости в запасном варианте __call().

Я уверен, что есть и другие подходы, но этот хорошо сработал для меня.

person Jason    schedule 31.12.2019
comment
Теоретически можно написать декоратор, который будет принимать параметры, совместимые с Guzzle, и применять те же функции и возможности, что и Guzzle. Тем не менее, на самом деле необходимая функциональность на уровнях предпочтительнее, IMO, но, возможно, эти уровни все еще могут читать конфигурацию, совместимую с Guzzle, чтобы облегчить миграцию. - person Jason; 31.12.2019
comment
Отличный ответ! Существуют ли отличные образцы PHP SDK (клиентские API-библиотеки), которые реализуют PSR-18/PSR-17/PSR-7? - person Nicodemuz; 12.11.2020