使用PSR 7、17和18而不是Guzzle的客户端不可知API包装器

问题描述 投票:0回答:1

PSR

PSR-7PSR-17PSR-18的引入都是使成为可能的计划的一部分

构建需要向服务器中的服务器发送HTTP请求的应用程序HTTP客户端不可知方式

请参见PSR-18: The PHP standard for HTTP clients

我一直在使用许多过去一直高度依赖Guzzle而不是抽象接口的应用程序。这些应用程序中的大多数都使用包含JSON主体的GET或POST请求以及还包含JSON主体的响应或包含HTTP 4xx或5xx错误的抛出异常来发出简单的API请求。

API包装器

这个问题来自一个最近的项目,我试图开发一个不显式依赖Guzzle而是仅依赖PSR接口的API包。

想法是创建一个可以使用以下方法初始化的类ApiWrapper

  1. 一个HTTP客户端实现PSR-18 ClientInterface
  2. A 请求工厂满足PSR-17 RequestFactoryInterface
  3. A Stream Factory满足PSR-17 StreamFactoryInterface

此类需要任何内容​​:

  1. 使用请求工厂流工厂发出请求(PSR-7)>>
  2. 使用HTTP客户端
  3. 发送请求
  4. 处理响应-因为我们知道这将满足PSR-7 ResponseInterface
  5. 这样的API包装器将不依赖于上述接口的任何具体实现,而仅需要这些接口的任何实现。因此,开发人员将能够使用他或她喜欢的HTTP客户端,而不必被迫使用Guzzle这样的特定客户端。

问题

现在,首先,我真的很喜欢Guzzle,这不是一个质疑Guzzle的强大功能的帖子,这只是一则帖子,询问如何使开发人员能够根据他们的需求选择正确的http客户端。] >

但是问题在于,由于Guzzle的功能不仅仅限于上述内容,因此明确依赖Guzzle可以提供很多不错的功能。 Guzzle还应用了handlers and middlewares范围,例如跟随重定向或对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);
    }
}

PSR引入PSR-7,PSR-17和PSR-18都是计划的一部分,该计划使构建需要以与HTTP客户端无关的方式向服务器发送HTTP请求的应用程序成为可能,请参阅...] >

这是我的看法,主要是基于尝试一些方法。

任何PSR-18 client将具有必须符合的接口。该接口实质上只是一种方法-sendRequest()。该方法将发送一个PSR-7 request并返回PSR-7 response

请求中的大部分内容将用于构建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客户端。

    枪口很棒,具有很多功能,并且在这种尊重。我相信PSR-18的做法应使我们能够将所有这些功能分解为较小的独立块因此可以根据需要应用它们。装饰器管理程序包可能有助于添加这些装饰器(也许确保它们订购正确并且彼此兼容)也许以不同的方式处理装饰器自定义方法以避免需要__call()后备广告。

    我确定还有其他方法,但是这种方法对我来说效果很好。

    php guzzle psr-7
    1个回答
    0
    投票

    这是我的看法,主要是基于尝试一些方法。

    © www.soinside.com 2019 - 2024. All rights reserved.