tomb1n0/generic-api-client

一个用于在消费API时加速开发的包

0.8 2024-05-15 13:23 UTC

README

Latest Version on Packagist Total Downloads

在开发我的PHP应用程序的集成时,我经常发现自己带着非常相似但略有不同的样板代码。

通常,每个实现都需要

  • 某种处理身份验证的方法。
    • 通常只是添加一个额外的头部或属性到请求体。
  • 分页处理
    • 通常归结为在获取下一页之前,检查响应头部或请求体中是否存在某些属性。
  • 响应模拟
    • 提供信心,确保我们的集成按预期工作,并且错误条件得到适当处理。

还有关于使用哪个HTTP客户端的问题,随着PSR-7PSR-17PSR-18的引入,我们能够依赖实现这些接口的HTTP客户端和工厂,而不是依赖于任何特定的客户端。

我的目标是通过这个包提供一个围绕这些PSR接口的包装器,使编写API集成更加简单。

安装

请注意,此包默认情况下不需要HTTP客户端 - 而是依赖于虚拟包psr/http-client-implementationpsr/http-factory-implementation。这允许包不受客户端限制。

如果您不确定,我建议将guzzlehttp/guzzle与该包一起使用,因为它为上述虚拟包提供了实现。

composer require tomb1n0/generic-api-client guzzlehttp/guzzle

如果您的项目已经需要具有上述标准实现的HTTP客户端,则可以省略guzzle依赖。

使用

请注意,以下示例假设使用Guzzle作为PSR-18客户端等。您可以用自己的替换这些。

客户端实例化

use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Client as GuzzleHttpClient;

$api = new Client(
    new GuzzleHttpClient(), // PSR-18 Client that sends the PSR-7 Request
    new HttpFactory(), // A PSR-17 Request Factory used to create PSR-7 Requests
    new HttpFactory(), // A PSR-17 Response Factory used to create PSR-7 Responses
    new HttpFactory(), // A PSR-17 Stream Factory used to create the bodies of our PSR-7 requests
    new HttpFactory(), // a PSR-17 URI Factory used to create URIs.
);

发送JSON请求

$response = $api->json('GET', 'https://dummyjson.com/products');

if ($response->successful()) {
    $products = $response->json('products');
}

发送表单(x-www-form-urlencoded)请求

$response = $api->form('GET', 'https://dummyjson.com/products');

if ($response->successful()) {
    $products = $response->json('products');
}

直接使用PSR-7请求发送请求

$requestFactory = new GuzzleHttp\Psr7\HttpFactory();
$request = $requestFactory->createRequest('GET', 'https://example.com');

$response = $api->send($request);

if ($response->successful()) {
    // Do something with the response.
}

配置

基本URL

$client = $existingClient->withBaseUrl('https://dummyjson.com');

$response = $client->json('GET', '/products'); // Will make a request to https://dummyjson.com/products.

请注意,如果您尝试向与基本URL不同的完整URL发送请求,则基本URL将被忽略。

分页

您可以通过创建一个实现此包提供的PaginationHandlerContract接口的类来创建一个分页处理程序。

// Create a class that implements the PaginationHandlerContract
class PaginationHandler implements PaginationHandlerContract
{
    public function hasNextPage(Response $response): bool
    {
        return $response->toPsr7Response()->hasHeader('next-page');
    }

    public function getNextPage(Response $response): RequestInterface
    {
        $originalRequest = $response->toPsr7Request();
        $psr7Response = $response->toPsr7Response();

        return $originalRequest->withHeader('page', $psr7Response->getHeaderLine('next-page'));
    }
}
$handler = new PaginationHandler();
$client = $existingClient->withPaginationHandler($handler);

$response = $client->json('GET', 'https://dummyjson.com/products');

// HasNextPage will defer to the Pagination Handler to determine if the Response has a next page
if ($response->hasNextPage()) {
    $nextPage = $response->getNextPage();
}

// For convenience, a helper is provided to fetch all pages in a loop:
$response->forEachPage(function (Response $response) {
    // Do something with this pages response
});

中间件

可以通过创建一个实现MiddlewareContract接口的类来创建中间件。

class AuthenticationMiddleware implements MiddlewareContract
{
    public function __construct(protected string $accessToken)
    {
    }

    public function handle(RequestInterface $request, callable $next): ResponseInterface
    {
        // Mutate the request
        $request = $request->withHeader('Authorization', 'Bearer ' . $this->accessToken);

        // Call the next middleware in the chain, ultimately fetching the Response.
        $response = $next($request);

        // Can also mutate the Response here if desired.
        $response = $response->withHeader('X-My-Header', 'Foo');

        // Return the Response
        return $response;
    }
}

// Multiple middleware can be provided
$client = $existingClient->withMiddleware([
    new AuthenticationMiddleware('my-access-token');
]);

// The request will be sent through our middleware in the order given.
$response = $client->json('GET', 'https://dummyjson.com/products');

请注意,中间件可以在发送请求之前或接收响应之后修改请求。

测试API

模拟响应

为了测试目的,可以模拟响应。

// It is important to call fake first, as this returns a new client with a Fake PSR-18 client underneath.
$client = $existingClient->fake()->stubResponse(
    'https://dummyjson.com/products',
    [
        'products' => [['id' => 1], ['id' => 2]],
    ],
    200,
    ['X-Custom-Header' => 'Foo'],
);

$response = $client->json('GET', 'https://dummyjson.com/products');

if ($response->successful()) {
    $products = $response->json('products');
}

防止意外请求

默认情况下,库对于任何未匹配的模拟响应将返回200 OK。如果您愿意,可以防止意外请求。

$client = $existingClient->fake()->preventStrayRequests();

try {
    // Make a request which has not been stubbed
    $response = $client->json('GET', 'https://dummyjson.com/products');
} catch (NoMatchingStubbedResponseException $e) {
    // a NoMatchingStubbedResponseException exception will be thrown.
}

断言请求

也许您想断言正确的有效负载已发送到API以创建用户。

$client = $existingClient->fake()->stubResponse('https://dummyjson.com/users', null, 200);

// This would likely be in some Service object method your test is calling.
$response = $client->json('POST', 'https://dummyjson.com/users', ['name' => 'Tom']);

// Assert we sent a request with the correct payload
$client->assertSent(function (RequestInterface $request) {
    $contents = $request->getBody()->getContents();
    $expected = ['name' => 'Tom'];

    return $contents === $expected
});

使用自定义请求匹配断言请求

可能你想使用请求中的其他信息来模拟一个响应

与上面相同,但使用stubResponseWithCustomMatcher方法,提供一个自定义的匹配器合约的实现。例如,你可以使用包含的UrlMatcher来检查方法类型

class UrlMatcher implements FakeResponseMatcherContract
{
    public function __construct(private string $url, private ?string $method = 'GET')
    {
    }

    public function match(RequestInterface $request): bool
    {
        $requestUrl = (string) $request->getUri();
        $requestMethod = $request->getMethod();

        return $this->url === $requestUrl && $this->method === $requestMethod;
    }
}
$client = $existingClient->fake();
$client->stubResponseWithCustomMatcher(new UrlMatcher('https://dummyjson.com/users', 'GET'), null, 200);
$client->stubResponseWithCustomMatcher(new UrlMatcher('https://dummyjson.com/users', 'POST'), null, 500);

运行测试

composer test

鸣谢

许可协议

MIT许可协议(MIT)。有关更多信息,请参阅许可文件