phpro/http-tools

用于开发更一致的HTTP实现的HTTP工具。

2.5.0 2024-09-09 12:53 UTC

This package is auto-updated.

Last update: 2024-09-09 12:54:41 UTC


README

Github Actions Installs Packagist

HTTP-Tools

该包的目的是为您提供一些工具来设置以数据为中心的、一致的HTTP集成。您想要使用的HTTP客户端实现只是一个小的实现细节,并不重要。然而,这里有一些默认指南

先决条件

选择您想要使用的HTTP包完全取决于您。我们要求使用PSR实现来安装此包

  • PSR-7: psr/http-message-implementationnyholm/psr7guzzlehttp/psr7
  • PSR-17: psr/http-factory-implementationnyholm/psr7guzzlehttp/psr7
  • PSR-18: psr/http-client-implementationsymfony/http-clientguzzlehttp/guzzle

安装

composer require phpro/http-tools

设置HTTP客户端

您可以选择任何您想要的HTTP客户端。然而,此包提供了一些方便的工厂,使配置更加简单。

工厂接受一个特定实现插件/中间件的列表。除此之外,您还可以配置特定实现的选项,如基础URI或默认头。

<?php

use Phpro\HttpTools\Client\Configurator\PluginsConfigurator;
use Phpro\HttpTools\Client\Factory\AutoDiscoveredClientFactory;
use Phpro\HttpTools\Client\Factory\GuzzleClientFactory;
use Phpro\HttpTools\Client\Factory\SymfonyClientFactory;

$options = ['base_uri' => $_ENV['SOME_CLIENT_BASE_URI']];

$httpClient = AutoDiscoveredClientFactory::create($middlewares);
$httpClient = GuzzleClientFactory::create($guzzlePlugins, $options);
$httpClient = SymfonyClientFactory::create($middlewares, $options);

// If you are using guzzle, you can both use guzzle and httplug plugins.
// You can wrap additional httplug plugins like this:
$httpClient = PluginsConfigurator::configure($httpClient, $middlewares);

如果您想有更多的控制或想使用其他工具,您始终可以创建自己的工厂!

注意:此包不下载特定的HTTP实现。您可以选择任何您想要的包,但您必须手动将其添加到composer。

通过插件配置客户端

插件-中间件 : Patato-Patato.

如果您想扩展HTTP客户端的工作方式,我们希望您使用插件!您可以使用插件做任何事情:日志记录、身份验证、语言指定等...

示例

<?php

$middlewares = [
    new Phpro\HttpTools\Plugin\AcceptLanguagePlugin('nl-BE'),
    new App\SomeClient\Plugin\Authentication\ServicePrincipal($_ENV['API_SECRET']),
];

内置插件:

  • AcceptLanguagePlugin:使得可以向请求添加Accept-Language。
  • CallbackPlugin:使得可以将简单的callable提升为真正的Plugin

记住已经有很多HTTPlug中间件可用。在编写自己的之前,尝试使用其中之一!

日志记录

此包包含php-http/logger-plugin。此外,我们还添加了一些装饰器,可以帮助您从日志中删除敏感信息。您可以通过指定一个debug参数从完整日志切换到简单日志!

<?php

use Phpro\HttpTools\Formatter\RemoveSensitiveHeadersFormatter;
use Phpro\HttpTools\Formatter\RemoveSensitiveJsonKeysFormatter;

$middlewares[] = new Http\Client\Common\Plugin\LoggerPlugin(
    $logger,
    new RemoveSensitiveHeadersFormatter(
        new RemoveSensitiveJsonKeysFormatter(
            BasicFormatterFactory::create($debug = true, $maxBodyLength = 1000),
            ['password', 'oldPassword', 'refreshToken']
        ),
        ['X-Api-Key', 'X-Api-Secret']
    )
); 

更多信息...

使用HTTP客户端

我们不希望您直接使用PSR-18客户端!相反,我们建议您使用请求处理器原则。那么这种架构是什么样的呢?

Architecture

  • 模型:可以用来包装外出或传入数据(数组、对象、字符串等)的请求/响应值对象。
  • 请求处理器:通过使用传输将请求数据模型转换为响应数据模型。您也可以在那里添加错误处理。
  • 传输:将请求数据模型转换为PSR-7 HTTP请求,并通过实际的HTTP客户端请求响应。
  • 编码:传输可以接受编码器/解码器,它们负责将值对象数据转换为例如JSON有效载荷,反之亦然。
  • HTTP客户端:您想使用哪种PSR-18 HTTP客户端:guzzle、curl、symfony/http-client等

关于传输和编码的更多信息

通过使用这种架构,我们提供了一个易于扩展的流程,其中包含用模型替换繁琐的数组结构。

您可能熟悉一个“客户端”类,它提供对多个API端点的访问。我们认为这种方法是一个多请求处理器类。您可以选择这种方法,但是,我们建议为每个API端点使用1个请求处理器。这样,您只需将当时需要注入/模拟的东西放入代码库中。

示例实现

<?php
use Phpro\HttpTools\Transport\Presets\JsonPreset;
use Phpro\HttpTools\Uri\TemplatedUriBuilder;

$transport = App\SomeClient\Transport\MyCustomTransportWrapperForDealingWithIsErrorPropertyEg(
    JsonPreset::create(
        $httpClient,
        new TemplatedUriBuilder()
    )
);

示例请求处理器

<?php

use Phpro\HttpTools\Transport\TransportInterface;

class ListSomething
{
    public function __construct(
        /**
         * TransportInterface<array, array>
         */
        private TransportInterface $transport
    ) {}

    public function __invoke(ListRequest $request): ListResponse
    {
        // You could validate the result first + throw exceptions based on invalid content
        // Tip : never trust APIs!
        // Try to gracefully fall back if possible and keep an eye on how the implementation needs to handle errors!

        return ListResponse::fromRawArray(
            ($this->transport)($request)
        );    
    }
}
<?php

use Phpro\HttpTools\Request\RequestInterface;

// By wrapping the request in a Value Object, you can use named constructors to pass in filters and POST data.
// You can add multiple named constructors if you want the list to behave in different ways in some cases.

/**
 * @implements RequestInterface<array>
 */
class ListRequest implements RequestInterface
{
    public function method() : string
    {
        return 'GET';
    }

    public function uri() : string
    {
        return '/list{?query}'; 
    }

    public function uriParameters() : array
    {
        return [
            'query' => 'somequery',
        ];
    }

    public function body() : array
    {
        return [];
    }
}

// By wrapping the response in a Value Object, you can sanitize and normalize data.
// You could as well lazilly throw an exception in here if some value is missing.
// However, that's might be more of a task for a request-handler.

class ListResponse
{
    public static function fromRawArray(array $data): self
    {
        return new self($data);    
    }

    public function getItems(): array
    {
        // Never trust APIs!
        return (array) ($this->data['items'] ?? []);
    }
}

这个示例相当简单,一开始可能看起来有些过度。一旦您在请求模型中创建了多个命名构造函数和条件属性访问器,真正的力量就会显现出来。如果精心构建响应模型,将提高您集成的稳定性!

异步请求处理器

为了发送异步请求,您可以结合使用此包和基于fibers的PSR-18客户端。架构可以保持不变。

一个基于ReactPHP的示例客户端可能基于此

composer require react/async veewee/psr18-react-browser

(目前还没有AMP或ReactPHP的官方基于fibers的PSR-18实现。因此,可以使用一个小型桥接器作为中间解决方案

由于fibers处理异步部分,您可以像编写同步请求处理器一样编写请求处理器

<?php

use Phpro\HttpTools\Transport\TransportInterface;

class FetchSomething
{
    public function __construct(
        /**
         * TransportInterface<array, array>
         */
        private TransportInterface $transport
    ) {}

    public function __invoke(FetchRequest $request): Something
    {
        return Something::tryParse(
            ($this->transport)($data)
        );
    }
}

为了获取多个同时请求,您可以并行执行这些操作

use Phpro\HttpTools\Transport\Presets\JsonPreset;
use Phpro\HttpTools\Uri\RawUriBuilder;
use Phpro\HttpTools\Uri\TemplatedUriBuilder;
use Veewee\Psr18ReactBrowser\Psr18ReactBrowserClient;
use function React\Async\async;
use function React\Async\await;
use function React\Async\parallel;

$client = Psr18ReactBrowserClient::default();
$transport = JsonPreset::create($client, new TemplatedUriBuilder());
$handler = new FetchSomething($transport);

$run = fn($id) => async(fn () => $handler(new FetchRequest($id)));
$things = await(parallel([
    $run(1),
    $run(2),
    $run(3),
]));

如果您的客户端与fibers兼容,这将并行获取所有请求。如果您的客户端不与fibers兼容,这将导致请求依次执行。

SDK

在某些情况下,编写请求处理器可能有些过度。此包还提供了一些工具,可以组合更通用的API客户端。然而,我们的主要建议是创建特定的请求处理器!

有关创建SDK的更多信息

测试HTTP客户端

此工具为使用PHPUnit对API客户端进行单元测试提供了一些特性。

UseHttpFactories

此特性可以帮助您在测试中构建请求和响应,而无需担心您使用的HTTP包

  • createRequest
  • createResponse
  • createStream
  • createEmptyHttpClientException

UseHttpToolsFactories

此特性可以帮助您在单元测试中构建特定的HTTP工具对象。例如,可以用来测试传输。

  • createToolsRequest

示例

$request = $this->createToolsRequest('GET', '/some-endpoint', [], ['hello' => 'world']);

UseMockClient

包含UseHttpFactories特性

最好用它来测试您自己的中间件和传输。也有可能测试请求处理器,但您必须手动提供响应。

示例

<?php
use Http\Mock\Client;
use \Phpro\HttpTools\Test\UseMockClient;

class SomeTest extends TestCase
{
    use UseMockClient;
    
    protected function setUp(): void
    {
        // You can configure the mock client through a callback.
        // Or you can skip the callback and configure the result of this method.
        $this->client = $this->mockClient(function (Client $client): Client {
            $client->setDefaultException(new \Exception('Dont call me!'));
            return $client;
        });
    }    
}

更多信息...

UseVcrClient

包含UseHttpFactories特性

此客户端可以用于使用实时数据测试请求处理器。第一次在测试中使用它时,它将执行实际的HTTP请求。此请求的响应将被记录并存储在您的项目中。第二次测试运行时,它将使用记录的版本。

示例

<?php
use Http\Client\Plugin\Vcr\NamingStrategy\PathNamingStrategy;
use Phpro\HttpTools\Client\Factory\AutoDiscoveredClientFactory;
use Phpro\HttpTools\Test\UseVcrClient;

class SomeTest extends TestCase
{
    use UseVcrClient;
    
    protected function setUp(): void
    {
        // Instead of the autodiscover client, you can use your own client factory.
        // That way, you can e.g. add the required authentication, ...
        $this->client = AutoDiscoveredClientFactory::create([
            ...$this->useRecording(FIXTURES_DIR, new PathNamingStrategy())        
        ]);
    }
}

更多信息...