构建可重用的 API 请求和客户端类
最近一直致力于集成第三方 API。有多种不同的方法可以实现此目的,例如使用第三方提供的 SDK。然而,我觉得坚持 Laravel 的Http外观通常是更好的选择。通过使用Http外观,所有第三方集成都可以具有类似的结构,并且测试和模拟变得更加容易。此外,您的应用程序将具有更少的依赖项。您不必担心如何使 SDK 保持最新状态,也不必担心 SDK 不再受支持时该怎么办。
使用 集成 Google Books API 作为示例,创建一个可重用的客户端和请求类,以使 API 的使用变得非常简单
让我们开始吧!

将 Google Books 配置添加到 Laravel
现在我们有了 API 密钥,我们可以将其.env与 API URL 一起添加到中。
GOOGLE_BOOKS_API_URL=https://www.googleapis.com/books/v1 GOOGLE_BOOKS_API_KEY=[API KEY FROM GOOGLE]
在此示例中,我存储了从 Google Cloud 控制台获取的 API 密钥,但我们将访问的 API 部分不需要该密钥。对于更高级的 API 使用,您需要与 Google 的 OAuth 2.0 服务器集成,并创建也可以存储在文件中的客户端 ID 和密钥.env。这超出了本文的范围。
环境变量就位后,打开 config/services.php 文件并添加 Google Books 的部分。
'google_books' => [
// 从 .env 检索的 Google Books API 的基本 URL
'base_url' => env('GOOGLE_BOOKS_API_URL'),
// Google Books API 的 API 密钥,从 .env 检索
'api_key' => env('GOOGLE_BOOKS_API_KEY'),
],创建 ApiRequest 类
当向 API 发出请求时,我发现使用一个简单的类来设置我需要的任何请求属性是最简单的。
下面是一个ApiRequest类的示例,我用它来传递 URL 信息以及正文、标头和任何查询参数。可以轻松修改或扩展此类以添加附加功能。
<?php
namespace App\Support;
/**
* ApiRequest 类是一个用于构建对 API 的 HTTP 请求的实用程序。
* 提供设置HTTP方法、URI、标头、查询的方法
* 请求的参数和正文。
* 它还提供了获取这些属性的方法,以及
* 清除标头、查询参数和正文。
* 此外,它还提供了创建ApiRequest实例的静态方法
* 对于特定的 HTTP 方法。
*/
class ApiRequest
{
// 存储将与 API 请求一起发送的标头。
protected array $headers = [];
//存储任何查询字符串参数。
protected array $query = [];
// 存储请求的正文。
protected array $body = [];
/**
* 为给定的 HTTP 方法和 URI 创建 API 请求。
*/
public function __construct(protected HttpMethod $method = HttpMethod::GET, protected string $uri = '')
{
}
/**
* 设置请求的标头。
* 这接受键和值,或键/值对数组。
*/
public function setHeaders(array|string $key, string $value = null): static
{
if (is_array($key)) {
$this->headers = $key;
} else {
$this->headers[$key] = $value;
}
return $this;
}
/**
* 清除请求的标头。
* 该方法可以清除请求中的特定标头或所有标头,如果
* 不提供钥匙。
*/
public function clearHeaders(string $key = null): static
{
if ($key) {
unset($this->headers[$key]);
} else {
$this->headers = [];
}
return $this;
}
/**
* 设置请求的查询参数。
* 这接受键和值,或键/值对数组。
*/
public function setQuery(array|string $key, string $value = null): static
{
if (is_array($key)) {
$this->query = $key;
} else {
$this->query[$key] = $value;
}
return $this;
}
/**
* 清除请求的查询参数。
* 该方法可以清除某个参数或者某个按键的所有参数
* 不提供。
*/
public function clearQuery(string $key = null): static
{
if ($key) {
unset($this->query[$key]);
} else {
$this->query = [];
}
return $this;
}
/**
* 设置请求的正文数据。
* 这接受键和值,或键/值对数组。
*/
public function setBody(array|string $key, string $value = null): static
{
if (is_array($key)) {
$this->body = $key;
} else {
$this->body[$key] = $value;
}
return $this;
}
/**
* 清除请求的正文数据。
* 该方法可以清除特定键的数据或全部数据。
*/
public function clearBody(string $key = null): static
{
if ($key) {
unset($this->body[$key]);
} else {
$this->body = [];
}
return $this;
}
/**
* 此方法返回 API 请求的标头。
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* 此方法返回 API 请求的查询。
*/
public function getQuery(): array
{
return $this->query;
}
/**
* 此方法返回 API 请求的正文。
*/
public function getBody(): array
{
return $this->body;
}
/**
* 该方法返回API请求的URI。
* 如果查询为空,或者我们有一个GET请求,可以返回URI
* 按原样。
* 否则,我们需要将查询字符串附加到 URI 中。
*/
public function getUri(): string
{
if (empty($this->query) || $this->method === HttpMethod::GET) {
return $this->uri;
}
return $this->uri.'?'.http_build_query($this->query);
}
/**
* This method returns the HTTP method for the API request.
*/
public function getMethod(): HttpMethod
{
return $this->method;
}
// 以下方法用于创建特定 HTTP 的 API 请求
// 方法。
public static function get(string $uri = ''): static
{
return new static(HttpMethod::GET, $uri);
}
public static function post(string $uri = ''): static
{
return new static(HttpMethod::POST, $uri);
}
public static function put(string $uri = ''): static
{
return new static(HttpMethod::PUT, $uri);
}
public static function delete(string $uri = ''): static
{
return new static(HttpMethod::DELETE, $uri);
}
}类构造函数采用一个HttpMethod,它只是一个包含各种 HTTP 方法的简单枚举和一个 URI。
enum HttpMethod: string
{
case GET = 'get';
case POST = 'post';
case PUT = 'put';
case DELETE = 'delete';
}有一些辅助方法可以使用 HTTP 方法名称并传递 URI 创建请求。最后,还有添加和清除标头、查询参数和正文数据的方法。
创建 API 客户端
现在我们有了请求,我们需要一个 API 客户端来发送它。这是我们可以使用Http门面的地方。
抽象ApiClient
首先,我们将创建一个抽象ApiClient类,该类将通过我们的各种 API 进行扩展。
<?php
namespace App\Support;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
/**
* ApiClient 类是一个用于向 API 发送 HTTP 请求的抽象基类。
* 它提供了一个发送 ApiRequest 的方法,并提供了获取和授权基本请求的方法。
* 子类必须实现 baseUrl 方法来指定 API 的基本 URL。
*/
abstract class ApiClient
{
/**
* 发送 ApiRequest 到 API 并返回响应。
*/
public function send(ApiRequest $request): Response
{
return $this->getBaseRequest()
->withHeaders($request->getHeaders())
->{$request->getMethod()->value}(
$request->getUri(),
$request->getMethod() === HttpMethod::GET
? $request->getQuery()
: $request->getBody()
);
}
/**
* 获取 API 的基本请求。
* 这个方法对于 API 请求有一些有用的默认值。
* 基本请求是一个具有 JSON 接受、内容类型为 'application/json' 和 API 的基本 URL 的 PendingRequest。
* 它还会针对非成功的响应抛出异常。
*/
protected function getBaseRequest(): PendingRequest
{
$request = Http::acceptJson()
->contentType('application/json')
->throw()
->baseUrl($this->baseUrl());
return $this->authorize($request);
}
/**
* 授权 API 请求。
* 这个方法用于被子类重写,以提供特定于 API 的授权。
* 默认情况下,它只是返回给定的请求。
*/
protected function authorize(PendingRequest $request): PendingRequest
{
return $request;
}
/**
* 获取 API 的基本 URL。
* 子类必须实现这个方法来提供 API 的基本 URL。
*/
abstract protected function baseUrl(): string;
}此类有一个 getBaseRequest 方法,可以使用外观创建一些合理的默认值 Http 来创建 PendingRequest. 它调用authorize我们可以在 Google Books 实现中重写的方法来设置 API 密钥。
该 baseUrl 方法只是一个简单的抽象方法,我们的 Google Books 类将设置它以使用我们之前设置的 Google Books API URL。
最后,send 方法是将请求发送到 API 的方法。它需要一个ApiRequest参数来构建请求,然后返回响应。
GoogleBooksApi客户端
创建抽象客户端后,我们现在可以创建一个GoogleBooksApiClient来扩展它。
<?php
namespace App\Support;
use Illuminate\Http\Client\PendingRequest;
/**
* GoogleBooksApiClient 类是一个对 Google Books API 的 ApiClient 基类的具体实现。
* 它提供了获取基本 URL 和授权请求的方法,用于操作 Google Books API。
*/
class GoogleBooksApiClient extends ApiClient
{
/**
* 获取 Google Books API 的基本 URL。
* 基本 URL 是从 'services.google_books.base_url' 配置值中获取的。
*/
protected function baseUrl(): string
{
return config('services.google_books.base_url');
}
/**
* 授权 Google Books API 的请求。
* Google Books API 将 API 密钥作为名为 'key' 的查询参数接受。
* API 密钥是从 'services.google_books.api_key' 配置值中获取的。
*/
protected function authorize(PendingRequest $request): PendingRequest
{
return $request->withQueryParameters([
'key' => config('services.google_books.api_key'),
]);
}
}在这个类中,我们只需要设置基本URL并配置授权。对于 Google Books API,这意味着将 API 密钥作为 URL 参数传递并设置空 Authorization 标头。
如果我们有一个使用不记名授权的 API,我们可以有authorize如下方法:
protected function authorize(PendingRequest $request): PendingRequest
{
return $request->withToken(config(services.someApi.token));
}使用此方法的好处 authorize 是它可以灵活地支持各种 API 授权方法。
按书名查询书籍
现在我们有了ApiRequest类 和GoogleBooksApiClient,我们可以创建一个操作来按标题查询书籍。它看起来像这样:
<?php
namespace App\Actions;
use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Http\Client\Response;
/**
* QueryBooksByTitle 类是一个从 Google Books API 查询书籍标题的操作类。
* 它提供了一个 __invoke 方法,接受一个标题,并返回 API 的响应。
*/
class QueryBooksByTitle
{
/**
* 从 Google Books API 查询书籍标题并返回响应。
* 此方法创建了一个 GoogleBooksApiClient 和一个针对 'volumes' 终点的 ApiRequest,
* 使用给定的标题作为 'q' 查询参数,并将 'books' 设置为 'printType' 查询参数。
* 然后使用客户端发送请求并返回响应。
*/
public function __invoke(string $title): Response
{
$client = app(GoogleBooksApiClient::class);
$request = ApiRequest::get('volumes')
->setQuery('q', 'intitle:'.$title)
->setQuery('printType', 'books');
return $client->send($request);
}
}然后,为了调用该操作,如果我想查找有关我刚刚阅读并强烈推荐的《The Ferryman》一书的信息,请使用以下代码片段:
use App\Actions\QueryBooksByTitle;
$response = app(QueryBooksByTitle::class)("The Ferryman");
$response->json();奖励:测试
下面,我添加了一些用于测试请求和客户端类的示例。对于测试,我使用 Pest PHP,它在 PHPUnit 之上提供了干净的语法和附加功能。
API请求
<?php
use App\Support\ApiRequest;
use App\Support\HttpMethod;
it('sets request data properly', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders(['foo' => 'bar'])
->setQuery(['baz' => 'qux'])
->setBody(['quux' => 'quuz']);
expect($request)
->getHeaders()->toBe(['foo' => 'bar'])
->getQuery()->toBe(['baz' => 'qux'])
->getBody()->toBe(['quux' => 'quuz'])
->getMethod()->toBe(HttpMethod::GET)
->getUri()->toBe('/');
});
it('sets request data properly with a key->value', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders('foo', 'bar')
->setQuery('baz', 'qux')
->setBody('quux', 'quuz');
expect($request)
->getHeaders()->toBe(['foo' => 'bar'])
->getQuery()->toBe(['baz' => 'qux'])
->getBody()->toBe(['quux' => 'quuz'])
->getMethod()->toBe(HttpMethod::GET)
->getUri()->toBe('/');
});
it('clears request data properly', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders(['foo' => 'bar'])
->setQuery(['baz' => 'qux'])
->setBody(['quux' => 'quuz']);
$request->clearHeaders()
->clearQuery()
->clearBody();
expect($request)
->getHeaders()->toBe([])
->getQuery()->toBe([])
->getBody()->toBe([])
->getUri()->toBe('/');
});
it('clears request data properly with a key', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders('foo', 'bar')
->setQuery('baz', 'qux')
->setBody('quux', 'quuz');
$request->clearHeaders('foo')
->clearQuery('baz')
->clearBody('quux');
expect($request)
->getHeaders()->toBe([])
->getQuery()->toBe([])
->getBody()->toBe([])
->getUri()->toBe('/');
});
it('creates instance with correct method', function (HttpMethod $method) {
$request = ApiRequest::{$method->value}('/');
expect($request->getMethod())->toBe($method);
})->with([
[HttpMethod::GET],
[HttpMethod::POST],
[HttpMethod::PUT],
[HttpMethod::DELETE],
]);测试ApiRequest检查是否设置了正确的请求数据以及是否使用了正确的方法。
API客户端
测试ApiClient会稍微复杂一些。由于它是一个抽象类,我们将在函数中使用匿名类beforeEach来创建一个客户端来使用该扩展ApiClient。
请注意,我们也使用该Http::fake()方法。这会在Http外观上创建模拟,我们可以对其进行断言并防止在测试中发出 API 请求。
<?php
use App\Support\ApiClient;
use App\Support\ApiRequest;
use App\Support\HttpMethod;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
Http::fake();
$this->client = new class extends ApiClient
{
protected function baseUrl(): string
{
return 'https://example.com';
}
};
});
it('sends a get request', function () {
$request = ApiRequest::get('foo')
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::GET->name)
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('sends a post request', function () {
$request = ApiRequest::post('foo')
->setBody(['foo' => 'bar'])
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::POST->name)
->data()->toBe(['foo' => 'bar'])
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('sends a put request', function () {
$request = ApiRequest::put('foo')
->setBody(['foo' => 'bar'])
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::PUT->name)
->data()->toBe(['foo' => 'bar'])
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('sends a delete request', function () {
$request = ApiRequest::delete('foo')
->setBody(['foo' => 'bar'])
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::DELETE->name)
->data()->toBe(['foo' => 'bar'])
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('handles authorization', function () {
$client = new class extends ApiClient
{
protected function baseUrl(): string
{
return 'https://example.com';
}
protected function authorize(PendingRequest $request): PendingRequest
{
return $request->withHeaders(['Authorization' => 'Bearer foo']);
}
};
$request = ApiRequest::get('foo');
$client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)->header('Authorization')->toBe(['Bearer foo']);
return true;
});
});对于测试,我们确认在各种请求方法上正确设置了请求属性。我们还确认baseUrl和authorize方法被正确调用。为了做出这些断言,我们使用的Http::assertSent方法需要一个带有 a 的回调$request,我们可以对其进行测试。请注意,我正在使用 PestPHP 期望,然后返回true. 我们可以只使用正常的比较并返回它,但是通过使用期望,当测试失败时我们会得到更清晰的错误消息。阅读这篇优秀的文章以获取更多信息。
GoogleBooksApiClient测试
测试与我们只想确保正确处理自定义实现细节的测试GoogleBooksApiClient类似,例如设置基本 URL 并使用 API 密钥添加查询参数。ApiClient
另外,不是方法config中的助手beforeEach。通过使用帮助程序,我们可以为将在每个测试中使用的 Google 图书服务配置设置测试值。文章来源:https://www.toymoban.com/article/692.html
<?php
use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Request;
beforeEach(function () {
Http::fake();
config([
'services.google_books.base_url' => 'https://example.com',
'services.google_books.api_key' => 'foo',
]);
});
it('sets the base url', function () {
$request = ApiRequest::get('foo');
app(GoogleBooksApiClient::class)->send($request);
Http::assertSent(static function (Request $request) {
expect($request)->url()->toStartWith('https://example.com/foo');
return true;
});
});
it('sets the api key as a query parameter', function () {
$request = ApiRequest::get('foo');
app(GoogleBooksApiClient::class)->send($request);
Http::assertSent(static function (Request $request) {
expect($request)->url()->toContain('key=foo');
return true;
});
});总结
在本文中,我们介绍了在 Laravel 中集成第三方 API 的一些有用步骤。通过使用这些简单的自定义类以及外观Http,我们可以确保所有集成功能相似,更易于测试,并且不需要任何项目依赖项。在后面的文章中,我将通过介绍 DTO、使用模拟响应进行测试以及使用 API 资源来扩展这些集成技巧。文章来源地址https://www.toymoban.com/article/692.html
到此这篇关于使用 Laravel 的 Http Facade(门面) 简化 API 集成的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!












