2

PSR

PSR-7PSR-17PSR-18的引入都是计划的一部分,以使

构建需要以与 HTTP 客户端无关的方式向服务器发送 HTTP 请求的应用程序

请参阅PSR-18:HTTP 客户端的 PHP 标准

我一直在使用许多过去严重依赖 Guzzle 而不是抽象接口的应用程序。这些应用程序中的大多数使用包含 JSON 正文的 GET 或 POST 请求发出简单的 API 请求,响应也包含 JSON 正文或抛出 HTTP 4xx 或 5xx 错误的异常。

API 包装器

这个问题来自最近的一个项目,我尝试开发一个 API 包,该包没有明确依赖 Guzzle,而是仅依赖于 PSR 接口。

这个想法是创建一个ApiWrapper可以使用以下方法启动的类:

  1. 满足 PSR-18的HTTP 客户端ClientInterface
  2. 满足 PSR-17的请求工厂RequestFactoryInterface
  3. 满足 PSR-17的流工厂StreamFactoryInterface

这个类将有它需要的任何东西:

  1. 使用请求工厂流工厂发出请求 (PSR-7)
  2. 使用HTTP 客户端发送请求
  3. 处理响应 - 因为我们知道这将满足 PSR-7ResponseInterface

这样的 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);
    }
}
4

1 回答 1

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()回退。

我敢肯定还有其他方法,但这个方法对我来说效果很好。

于 2019-12-31T15:47:08.180 回答