nsrosenqvist/php-api-toolkit

一个易于集成远程API的多功能工具包

1.0.0 2022-12-10 16:32 UTC

This package is auto-updated.

Last update: 2024-09-27 20:43:08 UTC


README

此工具包是Guzzle 7的包装器,允许您使用特定远程API的专用客户端进行工作。它以无缝的方式自动应用配置,并保持代码模块化和整齐。此库的目标是消除对API特定SDK的需求,而是提供您开箱即用的几乎所有功能,包括一个全面的模拟中间件。

安装

此库需要PHP 7.4+,可以使用composer进行安装

composer require nsrosenqvist/php-api-toolkit

用法

核心概念是每个远程API及其相关的配置和授权都包含在一个继承自 NSRosenqvist\ApiToolkit\ADK 的单个类中。ADK类包含库利用的核心功能,但还包含一个功能更强大的基础类,它提供了通常需要的功能。因此,人们通常会基于 NSRosenqvist\ApiToolkit\StandardApi 来创建驱动程序,并且可以通过覆盖方法轻松进行任何所需的自定义。

简单的驱动程序只需要基本的客户端配置和授权方式

namespace MyApp\Api;

use NSRosenqvist\ApiToolkit\StandardApi;

class Example extends StandardApi
{
    public function __construct()
    {
        parent::__construct([
            'base_uri' => 'https://example.com',
            'http_errors' => false,
        ], [
            'cache' => true,
            'retries' => 3,
            'auth' => [
                getenv('EXAMPLE_USER'),
                getenv('EXAMPLE_PASS')
            ],
        ]);
    }

    // ...
}

父ADK类的第一个参数是一个定义Guzzle客户端选项的 array,第二个参数定义默认请求选项。所有默认中间件都通过请求选项进行配置。

现在可以通过在驱动类上调用常规Guzzle方法来使用此配置进行请求。

$example = new MyApp\Api\Example();

// Synchronous request
$response = $example->get('foo/bar', [
    'query' => ['id' => 100],
]);

// Asynchronous request
$promise = $example->getAsync('foo/bar', [
    'query' => ['id' => 100],
]);

$response = $promise->wait();

继承

没有私有方法,因为目的是让子类能够根据需要覆盖和修改基础库的功能。文档涉及到最常见的使用案例,但请参阅源代码以获得全面的概述。

处理器

通常,驱动程序只需要覆盖两个方法。第一个是 resultHandler,它处理同步和异步请求的结果。它需要返回一个 \Psr\Http\Message\ResponseInterface,下面的示例使用了内置的 \NSRosenqvist\ApiToolkit\Result 结构,该结构实现了接口并提供了一些额外的有用功能。

use Psr\Http\Message\ResponseInterface;
use NSRosenqvist\ApiToolkit\Structures\Result;
use NSRosenqvist\ApiToolkit\Structures\ListData;
use NSRosenqvist\ApiToolkit\Structures\ObjectData;

// ...

    public function resultHandler(ResponseInterface $response, array $options): Result
    {
        $type = $this->contentType($response);
        $data = $this->decode($response->getBody(), $type);

        if (is_array($data)) {
            $data = new ListData($data);
        } elseif (is_object($data)) {
            $data = new ObjectData($data);
        }

        return new Result($response, $data);
    }

当请求由于某些原因失败时,将调用错误处理器。在这里,您可以执行任何所需的操作,例如记录日志,然后返回异常或重新抛出它。

    public function errorHandler(RequestException $reason, array $options): RequestException
    {
        $this->logError($reason->getMessage());

        return $reason;
    }

请注意,如果将 http_errors 设置为false(如示例中所示),则错误处理器不会自动处理,而必须从结果处理器中手动调用。您可以在结果处理器中同时处理成功请求和错误,或者创建一个 \GuzzleHttp\Exception\RequestException 并将其传递给错误处理器。内置的记录中间件确保可以通过响应访问原始请求。

use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use NSRosenqvist\ApiToolkit\Structures\Result;

// ...

    public function resultHandler(ResponseInterface $response, array $options): Result
    {
        $result = new Result($response);

        if ($result->failure()) {
            $request = $response->getRequest();
            $exception = RequestException::create($request, $response);

            $this->errorHandler($exception);
        } else {
            // ...
        }

        return $result;
    }

管理器

一个管理类是内置的,它打算用作单例。使用此功能可以轻松地从代码库的各个位置访问API,最好通过依赖注入或外观(请参阅包含的 LaravelApiProvider 服务提供者)。

require 'vendor/autoload.php';

$manager = new NSRosenqvist\ApiToolkit\Manager();
$manager->registerDriver('example', '\\MyApp\\Api\\Example');

$example = $manager->get('example'); // instance of \MyApp\Api\Example

如果您将API注册为字符串的FQN并使用自动加载器,则它们在您实际使用之前不会被加载到内存中。

这个库的一个重要新增功能是请求链的概念。如果在驱动器上访问一个不存在的属性,将创建一个新的请求链。这些是请求的简单面向对象表示,适合在整合RESTful API时使用。所有常用的Guzzle魔法方法都得到支持,唯一不同的是它们不接受方法调用中的URI,而是从链中动态解析。

$api = $manager->get('example');

$result = $api->customers->get();        // GET /customers
$result = $api->customers->{100}->get(); // GET /customers/100

// The chain object is immutable, so one could even store a specific endpoint
// and make later chained requests based off of it
$settings = $api->settings; // Instance of \NSRosenqvist\ApiToolkit\RequestChain
$bar = $settings->foo->get();
$ipsum = $settings->lorem->get();

// Async calls works just as well
$promise = $api->customers->getAsync();

// There are also helper functions, these three all perform the same request
$deals = $api->deals->get([
    'query' => ['sort' => 'desc'],
]);
$deals = $api->deals->option('query', ['sort' => 'desc'])->get();
$deals = $api->deals->query(['sort' => 'desc'])->get();
$deals = $api->deals->queryArg('sort', 'desc')->get();

链的解析由发起的驱动器执行,因此要自定义解析方式,可以通过重载resolve方法来实现。

    // ... 

    public function resolve(RequestChain $chain): string
    {
        $options = $chain->getOptions();

        if (isset($options['api_version'])) {
            return 'v' . $options['api_version'] . '/' . $chain->resolve();
        } else {
            return $chain->resolve();
        }
    }

可以使用自定义选项轻松启动链,如果想要提供一个简单的便利方法来定义目标API版本,就像上面的例子一样,可以在驱动器上创建一个自定义方法,返回一个新的请求链。

use NSRosenqvist\ApiToolkit\RequestChain;

    // ...

    public function version($version = '1.5'): RequestChain
    {
        return $this->chain([
            'api_version' => (string) $version,
        ]);
    }

然后可以通过使用version方法启动链来简单地调用特定版本的API。

$result = $api->version(2)->customers->get();

模拟

使用此工具包构建的API很容易进行测试,可以将其模拟中间件包含在处理器堆栈中,或者基于\NSRosenqvist\ApiToolkit\StandardApi构建驱动器,以默认包含它。要使所有请求返回200(默认值),并且永远不会发送到远程服务器,只需通过请求选项mock或通过方法mocking切换它来启用模拟。

$api = $manager->get('httpbin');
$api->mocking(true);

$result = $api->status->{500}->get(); // Will return 200 instead of 500 

$api->mocking(false);

$result = $api->status->{500}->get(); // 500 Server Error

队列

更好的方法是使用队列系统预定义应返回的响应。当响应被排队时,模拟中间件将自动启用,之后不需要禁用它。

$api = $manager->get('httpbin');
$api->queue([
    $api->response(200),
    $api->response(201),
    $api->response(202),
]);

$result = $api->status->{500}->get(); // Will return 200
$result = $api->status->{500}->get(); // Will return 201
$result = $api->status->{500}->get(); // Will return 202

清单

模拟清单基本上是路由文件,它定义了对不同请求应提供哪些响应。它是一种相当灵活的语法,旨在帮助您尽可能少地编写配置。默认支持PHP和JSON文件,但您可以通过在composer.json文件中要求adhocore/json-commentsymfony/yaml来轻松启用JSONC或Yaml支持。

基本语法如下

{
    // Request URI
    "foo/bar": {
        "code": 200,                     // Return code
        "content": "Lorem Ipsum",        // Response body
        "headers": {                     // Response headers
            "Content-Type": "text/plain" // This isn't required, we will attempt to set the mime type automatically
        },
        "reasonPhrase": "OK",            // Reason phrase
        "version": "1.1",                // Protocol version
    }
}

content可以是简单的字符串,也可以是包含定义响应体的文件所在目录的路径(设置默认请求参数stubs)。绝对路径也可以正常工作。

变量匹配

可以通过将它们包裹在数组中并使用匹配语句来创建多个同一路由的响应定义。在下面的例子中,对foo/bar/lorem的请求将产生200响应,而对foo/bar/ipsum的请求将产生202。

{
    // Named variable in route definition
    "foo/bar/{var}": [
        {
            "code": 200,
            "match": {
                "var": "lorem"
            }
        },
        {
            "code": 202,
            "match": {
                "var": "ipsum"
            }
        }
    ]
}

“query”和“method”变量名是保留的,因为它们用于定义方法和方法查询匹配的条件。

通配符模式

您甚至可以混合使用命名变量和glob通配符。

{
    // Named variable in route definition
    "foo/bar/{var}": [
        // ...
    ],
    "foo/bar/*": {
        "code": 404
    }
}

路由将根据特定性进行匹配,因此命名变量将优先于通配符。底层通配符匹配使用fnmatch,因此可以使用更高级的模式,但它们不是官方支持的。

方法匹配

匹配定义中的方法键允许您为不同的方法指定不同的响应。

{
    "foo/bar": [
        {
            "code": 200,
            "match": {
                "method": "GET"
            }
        },
        {
            "code": 202,
            "match": {
                "method": "POST"
            }
        }
    ]
}
查询条件

除了变量匹配之外,还可以测试查询参数(这些也将根据特定性顺序优先)。

{
    "foo/bar": [
        {
            "code": 200,
            "match": {
                "query": {
                    "type": "foo"
                }
            }
        },
        {
            "code": 202,
            "match": {
                "query": {
                    "type": "bar"
                }
            }
        }
    ]
}
高级查询匹配

除了直接比较之外,还可以设置以下任何特殊比较运算符

  • __isset__:测试参数是否存在。
  • __missing__:测试参数是否存在。
  • __true__:测试参数是否为真(这包括“yes”、“y”、1等)。
  • __false__:测试参数是否为假(这包括“no”、“n”、0等)。
  • __bool__:测试参数是否为布尔值。
  • __string__:测试参数是否为字符串。
  • __numeric__:测试参数是否为数值。
  • __int__:测试参数是否为整数。
  • __float__:测试参数是否为浮点数。
  • __array__:测试参数是否为数组。
替代语法

为了最小化所需的配置,还支持一些替代语法。这些语法将在导入时进行标准化。

简短语法
{
    "alternative/syntax/short-code": 100,                // Will return a response with status 100
    "alternative/syntax/short-content": "Response body", // Will return a 200 response with custom body
    "alternative/syntax/short-stub": "file.txt",         // Will return a 200 response with body loaded from a file
}
REST 语法

REST 语法允许根据 HTTP 方法定义响应(方法将扩展为 match->method,类似于常规方法匹配)。此语法还支持简短语法的定义。

{
    "alternative/syntax/rest": {
        "GET": "response body",
        "POST": 204,
        "*": 404 // Catch-all definition is also supported
    }
}

结构

该库包含一些内置的通用数据结构,旨在消除自定义数据结构的需求。然而,它没有进行类型验证,但这可以很容易地在结果处理器中实现,例如使用 JSON Schema。

ObjectData

NSRosenqvist\ApiToolkit\Structures\ObjectData 类是一个通用对象,它提供了对其成员的属性和数组访问。这意味着以下使用它的方法都是等效的,代码示例将输出 "true"。

use NSRosenqvist\ApiToolkit\Structures\ObjectData;

$data = new ObjectData([
    'foo' => 'bar',
]);

if ($data['foo'] === $data->foo) {
    echo 'true';
}

它还实现了 Illuminate\Contracts\Support\ArrayableIlluminate\Contracts\Support\Jsonable,这使得它很容易被转换为数组(toArray)或 JSON(toJson)。

ListData

NSRosenqvist\ApiToolkit\Structures\ListData 类是一个通用列表数据结构。它支持数组访问,并且像 NSRosenqvist\ApiToolkit\Structures\ObjectData 一样,它也可以很容易地转换为数组或 JSON。

NSRosenqvist\ApiToolkit\Structures\ObjectDataNSRosenqvist\ApiToolkit\Structures\ListData 都支持宏。您可以定义自定义方法,这些方法将在类的所有实例(或如果您想按 API 驱动程序分别保留宏,则在子类)上可用。

use NSRosenqvist\ApiToolkit\Structures\ObjectData;

ObjectData::macro('html', function() {
    return "<p>{$this->foo}</p>";
});

$data = new ObjectData([
    'foo' => 'bar',
]);

echo $data->html(); // "<p>bar</p>"

结果

NSRosenqvist\ApiToolkit\Structures\Result 类是 \Psr\Http\Message\ResponseInterface 的实现,它提供了对底层响应体数据的直接访问。它定义了一些便利方法,如 successfailure,但它主要充当提供数据的包装器。如果数据是 NSRosenqvist\ApiToolkit\Structures\ListData,则可以直接迭代它,如果是 NSRosenqvist\ApiToolkit\Structures\ObjectData,则可以直接访问它。

use GuzzleHttp\Psr7\Response;
use NSRosenqvist\ApiToolkit\Structures\Result;
use NSRosenqvist\ApiToolkit\Structures\ListData;
use NSRosenqvist\ApiToolkit\Structures\ObjectData;

// ObjectData
$result = new Result(new Response(/* ... */), new ObjectData([
    'foo' => 'bar',
]));

echo $result->foo; // "bar"

// ListData
$result = new Result(new Response(/* ... */), new ListData([
    new ObjectData(['foo' => 'bar']),
    new ObjectData(['foo' => 'ipsum']),
]));

foreach ($result as $item) {
    echo $result->foo;
    // First iteration: "bar"
    // Second iteration: "ipsum"
}

包含的中间件

\NSRosenqvist\ApiToolkit\StandardApi 包含一些默认中间件,旨在提供常见的 API SDK 功能。

CacheMiddleware

包含的缓存机制是一个非常简单的实现,它确保在 PHP 请求过程中同一请求不会执行多次。它只为 GET 请求执行此操作。中间件可以通过使用选项标志 cache 来打开或关闭。

\NSRosenqvist\ApiToolkit\StandardApi 实现了一个名为 cache 的辅助方法,用于切换当前请求链的设置。

$api = $manager->get('example');

$customer = $api->cache(true)->customer->{100}->get(); // Fetched from remote
$customer = $api->cache(true)->customer->{100}->get(); // Fetched from cache
$customer = $api->cache(false)->customer->{100}->get(); // Since cache was set to false, it disregarded the cache and fetched it anew from the remote

MockMiddleware

模拟中间件的功能在模拟部分中描述。它使用请求选项为每个请求进行配置,但在 \NSRosenqvist\ApiToolkit\StandardApi 中自动处理。它接受的选项有

  • mock:启用或禁用中间件。
  • manifest:用于模拟路由的清单对象或路径。
  • stubs:清单可以返回内容的文件所在的目录。
  • response:直接定义的响应,将返回(由 StandardApi 的队列功能使用)。

RecorderMiddleware

记录器中间件简单地将响应替换为\NSRosenqvist\ApiToolkit\RetrospectiveResponse,同时也保留了当前请求的引用。这样做是为了在结果处理程序中能够执行与请求相关的处理。如果不需要这样做,您可以直接覆盖getHandlerStack方法并跳过包含此中间件。

通过检查选项endpoint,也可以实现类似的功能,该选项会自动设置在基于基本URI的请求上。对于绝对请求,将设置选项url

RetryMiddleware

如果远程返回429,则重试中间件将再次执行请求,最大重试次数和指数延迟。此中间件由选项max_retries控制。它还支持自定义重试决策回调函数。

许可证

该库在MIT许可证下发布,并且某些中间件基于Guzzle 7默认包含的中间件,也受MIT许可证的约束。某些数据结构基于Illuminate库中的数据结构,这些库也受MIT许可证的约束。

开发

我们非常欢迎贡献。如果您克隆了存储库,然后运行composer install,某些钩子应该会自动配置,确保在推送任何更改之前代码标准得到遵守。我们试图遵循PSR-12标准,并维持与PHP 7.4的兼容性。有关配置的命令(例如testlintcompat),请参阅composer.json