构建可重用的 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/diary/laravel/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/diary/laravel/692.html
到此这篇关于使用 Laravel 的 Http Facade(门面) 简化 API 集成的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!