構(gòu)建可重用的 API 請(qǐng)求和客戶端類(lèi)
最近一直致力于集成第三方 API。有多種不同的方法可以實(shí)現(xiàn)此目的,例如使用第三方提供的 SDK。然而,我覺(jué)得堅(jiān)持 Laravel 的Http外觀通常是更好的選擇。通過(guò)使用Http外觀,所有第三方集成都可以具有類(lèi)似的結(jié)構(gòu),并且測(cè)試和模擬變得更加容易。此外,您的應(yīng)用程序?qū)⒕哂懈俚囊蕾?lài)項(xiàng)。您不必?fù)?dān)心如何使 SDK 保持最新?tīng)顟B(tài),也不必?fù)?dān)心 SDK 不再受支持時(shí)該怎么辦。
使用 集成 Google Books API 作為示例,創(chuàng)建一個(gè)可重用的客戶端和請(qǐng)求類(lèi),以使 API 的使用變得非常簡(jiǎn)單
讓我們開(kāi)始吧!
將 Google Books 配置添加到 Laravel
現(xiàn)在我們有了 API 密鑰,我們可以將其.env與 API URL 一起添加到中。
GOOGLE_BOOKS_API_URL=https://www.googleapis.com/books/v1 GOOGLE_BOOKS_API_KEY=[API KEY FROM GOOGLE]
在此示例中,我存儲(chǔ)了從 Google Cloud 控制臺(tái)獲取的 API 密鑰,但我們將訪問(wèn)的 API 部分不需要該密鑰。對(duì)于更高級(jí)的 API 使用,您需要與 Google 的 OAuth 2.0 服務(wù)器集成,并創(chuàng)建也可以存儲(chǔ)在文件中的客戶端 ID 和密鑰.env。這超出了本文的范圍。
環(huán)境變量就位后,打開(kāi) 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'), ],
創(chuàng)建 ApiRequest 類(lèi)
當(dāng)向 API 發(fā)出請(qǐng)求時(shí),我發(fā)現(xiàn)使用一個(gè)簡(jiǎn)單的類(lèi)來(lái)設(shè)置我需要的任何請(qǐng)求屬性是最簡(jiǎn)單的。
下面是一個(gè)ApiRequest類(lèi)的示例,我用它來(lái)傳遞 URL 信息以及正文、標(biāo)頭和任何查詢(xún)參數(shù)??梢暂p松修改或擴(kuò)展此類(lèi)以添加附加功能。
<?php namespace App\Support; /** * ApiRequest 類(lèi)是一個(gè)用于構(gòu)建對(duì) API 的 HTTP 請(qǐng)求的實(shí)用程序。 * 提供設(shè)置HTTP方法、URI、標(biāo)頭、查詢(xún)的方法 * 請(qǐng)求的參數(shù)和正文。 * 它還提供了獲取這些屬性的方法,以及 * 清除標(biāo)頭、查詢(xún)參數(shù)和正文。 * 此外,它還提供了創(chuàng)建ApiRequest實(shí)例的靜態(tài)方法 * 對(duì)于特定的 HTTP 方法。 */ class ApiRequest { // 存儲(chǔ)將與 API 請(qǐng)求一起發(fā)送的標(biāo)頭。 protected array $headers = []; //存儲(chǔ)任何查詢(xún)字符串參數(shù)。 protected array $query = []; // 存儲(chǔ)請(qǐng)求的正文。 protected array $body = []; /** * 為給定的 HTTP 方法和 URI 創(chuàng)建 API 請(qǐng)求。 */ public function __construct(protected HttpMethod $method = HttpMethod::GET, protected string $uri = '') { } /** * 設(shè)置請(qǐng)求的標(biāo)頭。 * 這接受鍵和值,或鍵/值對(duì)數(shù)組。 */ public function setHeaders(array|string $key, string $value = null): static { if (is_array($key)) { $this->headers = $key; } else { $this->headers[$key] = $value; } return $this; } /** * 清除請(qǐng)求的標(biāo)頭。 * 該方法可以清除請(qǐng)求中的特定標(biāo)頭或所有標(biāo)頭,如果 * 不提供鑰匙。 */ public function clearHeaders(string $key = null): static { if ($key) { unset($this->headers[$key]); } else { $this->headers = []; } return $this; } /** * 設(shè)置請(qǐng)求的查詢(xún)參數(shù)。 * 這接受鍵和值,或鍵/值對(duì)數(shù)組。 */ public function setQuery(array|string $key, string $value = null): static { if (is_array($key)) { $this->query = $key; } else { $this->query[$key] = $value; } return $this; } /** * 清除請(qǐng)求的查詢(xún)參數(shù)。 * 該方法可以清除某個(gè)參數(shù)或者某個(gè)按鍵的所有參數(shù) * 不提供。 */ public function clearQuery(string $key = null): static { if ($key) { unset($this->query[$key]); } else { $this->query = []; } return $this; } /** * 設(shè)置請(qǐng)求的正文數(shù)據(jù)。 * 這接受鍵和值,或鍵/值對(duì)數(shù)組。 */ public function setBody(array|string $key, string $value = null): static { if (is_array($key)) { $this->body = $key; } else { $this->body[$key] = $value; } return $this; } /** * 清除請(qǐng)求的正文數(shù)據(jù)。 * 該方法可以清除特定鍵的數(shù)據(jù)或全部數(shù)據(jù)。 */ public function clearBody(string $key = null): static { if ($key) { unset($this->body[$key]); } else { $this->body = []; } return $this; } /** * 此方法返回 API 請(qǐng)求的標(biāo)頭。 */ public function getHeaders(): array { return $this->headers; } /** * 此方法返回 API 請(qǐng)求的查詢(xún)。 */ public function getQuery(): array { return $this->query; } /** * 此方法返回 API 請(qǐng)求的正文。 */ public function getBody(): array { return $this->body; } /** * 該方法返回API請(qǐng)求的URI。 * 如果查詢(xún)?yōu)榭?,或者我們有一個(gè)GET請(qǐng)求,可以返回URI * 按原樣。 * 否則,我們需要將查詢(xún)字符串附加到 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; } // 以下方法用于創(chuàng)建特定 HTTP 的 API 請(qǐng)求 // 方法。 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); } }
類(lèi)構(gòu)造函數(shù)采用一個(gè)HttpMethod,它只是一個(gè)包含各種 HTTP 方法的簡(jiǎn)單枚舉和一個(gè) URI。
enum HttpMethod: string { case GET = 'get'; case POST = 'post'; case PUT = 'put'; case DELETE = 'delete'; }
有一些輔助方法可以使用 HTTP 方法名稱(chēng)并傳遞 URI 創(chuàng)建請(qǐng)求。最后,還有添加和清除標(biāo)頭、查詢(xún)參數(shù)和正文數(shù)據(jù)的方法。
創(chuàng)建 API 客戶端
現(xiàn)在我們有了請(qǐng)求,我們需要一個(gè) API 客戶端來(lái)發(fā)送它。這是我們可以使用Http門(mén)面的地方。
抽象ApiClient
首先,我們將創(chuàng)建一個(gè)抽象ApiClient類(lèi),該類(lèi)將通過(guò)我們的各種 API 進(jìn)行擴(kuò)展。
<?php namespace App\Support; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; /** * ApiClient 類(lèi)是一個(gè)用于向 API 發(fā)送 HTTP 請(qǐng)求的抽象基類(lèi)。 * 它提供了一個(gè)發(fā)送 ApiRequest 的方法,并提供了獲取和授權(quán)基本請(qǐng)求的方法。 * 子類(lèi)必須實(shí)現(xiàn) baseUrl 方法來(lái)指定 API 的基本 URL。 */ abstract class ApiClient { /** * 發(fā)送 ApiRequest 到 API 并返回響應(yīng)。 */ 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 的基本請(qǐng)求。 * 這個(gè)方法對(duì)于 API 請(qǐng)求有一些有用的默認(rèn)值。 * 基本請(qǐng)求是一個(gè)具有 JSON 接受、內(nèi)容類(lèi)型為 'application/json' 和 API 的基本 URL 的 PendingRequest。 * 它還會(huì)針對(duì)非成功的響應(yīng)拋出異常。 */ protected function getBaseRequest(): PendingRequest { $request = Http::acceptJson() ->contentType('application/json') ->throw() ->baseUrl($this->baseUrl()); return $this->authorize($request); } /** * 授權(quán) API 請(qǐng)求。 * 這個(gè)方法用于被子類(lèi)重寫(xiě),以提供特定于 API 的授權(quán)。 * 默認(rèn)情況下,它只是返回給定的請(qǐng)求。 */ protected function authorize(PendingRequest $request): PendingRequest { return $request; } /** * 獲取 API 的基本 URL。 * 子類(lèi)必須實(shí)現(xiàn)這個(gè)方法來(lái)提供 API 的基本 URL。 */ abstract protected function baseUrl(): string; }
此類(lèi)有一個(gè) getBaseRequest 方法,可以使用外觀創(chuàng)建一些合理的默認(rèn)值 Http 來(lái)創(chuàng)建 PendingRequest. 它調(diào)用authorize我們可以在 Google Books 實(shí)現(xiàn)中重寫(xiě)的方法來(lái)設(shè)置 API 密鑰。
該 baseUrl 方法只是一個(gè)簡(jiǎn)單的抽象方法,我們的 Google Books 類(lèi)將設(shè)置它以使用我們之前設(shè)置的 Google Books API URL。
最后,send 方法是將請(qǐng)求發(fā)送到 API 的方法。它需要一個(gè)ApiRequest參數(shù)來(lái)構(gòu)建請(qǐng)求,然后返回響應(yīng)。
GoogleBooksApi客戶端
創(chuàng)建抽象客戶端后,我們現(xiàn)在可以創(chuàng)建一個(gè)GoogleBooksApiClient來(lái)擴(kuò)展它。
<?php namespace App\Support; use Illuminate\Http\Client\PendingRequest; /** * GoogleBooksApiClient 類(lèi)是一個(gè)對(duì) Google Books API 的 ApiClient 基類(lèi)的具體實(shí)現(xiàn)。 * 它提供了獲取基本 URL 和授權(quán)請(qǐng)求的方法,用于操作 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'); } /** * 授權(quán) Google Books API 的請(qǐng)求。 * Google Books API 將 API 密鑰作為名為 'key' 的查詢(xún)參數(shù)接受。 * API 密鑰是從 'services.google_books.api_key' 配置值中獲取的。 */ protected function authorize(PendingRequest $request): PendingRequest { return $request->withQueryParameters([ 'key' => config('services.google_books.api_key'), ]); } }
在這個(gè)類(lèi)中,我們只需要設(shè)置基本URL并配置授權(quán)。對(duì)于 Google Books API,這意味著將 API 密鑰作為 URL 參數(shù)傳遞并設(shè)置空 Authorization 標(biāo)頭。
如果我們有一個(gè)使用不記名授權(quán)的 API,我們可以有authorize如下方法:
protected function authorize(PendingRequest $request): PendingRequest { return $request->withToken(config(services.someApi.token)); }
使用此方法的好處 authorize 是它可以靈活地支持各種 API 授權(quán)方法。
按書(shū)名查詢(xún)書(shū)籍
現(xiàn)在我們有了ApiRequest類(lèi) 和GoogleBooksApiClient,我們可以創(chuàng)建一個(gè)操作來(lái)按標(biāo)題查詢(xún)書(shū)籍。它看起來(lái)像這樣:
<?php namespace App\Actions; use App\Support\ApiRequest; use App\Support\GoogleBooksApiClient; use Illuminate\Http\Client\Response; /** * QueryBooksByTitle 類(lèi)是一個(gè)從 Google Books API 查詢(xún)書(shū)籍標(biāo)題的操作類(lèi)。 * 它提供了一個(gè) __invoke 方法,接受一個(gè)標(biāo)題,并返回 API 的響應(yīng)。 */ class QueryBooksByTitle { /** * 從 Google Books API 查詢(xún)書(shū)籍標(biāo)題并返回響應(yīng)。 * 此方法創(chuàng)建了一個(gè) GoogleBooksApiClient 和一個(gè)針對(duì) 'volumes' 終點(diǎn)的 ApiRequest, * 使用給定的標(biāo)題作為 'q' 查詢(xún)參數(shù),并將 'books' 設(shè)置為 'printType' 查詢(xún)參數(shù)。 * 然后使用客戶端發(fā)送請(qǐng)求并返回響應(yīng)。 */ 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); } }
然后,為了調(diào)用該操作,如果我想查找有關(guān)我剛剛閱讀并強(qiáng)烈推薦的《The Ferryman》一書(shū)的信息,請(qǐng)使用以下代碼片段:
use App\Actions\QueryBooksByTitle; $response = app(QueryBooksByTitle::class)("The Ferryman"); $response->json();
獎(jiǎng)勵(lì):測(cè)試
下面,我添加了一些用于測(cè)試請(qǐng)求和客戶端類(lèi)的示例。對(duì)于測(cè)試,我使用 Pest PHP,它在 PHPUnit 之上提供了干凈的語(yǔ)法和附加功能。
API請(qǐng)求
<?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], ]);
測(cè)試ApiRequest檢查是否設(shè)置了正確的請(qǐng)求數(shù)據(jù)以及是否使用了正確的方法。
API客戶端
測(cè)試ApiClient會(huì)稍微復(fù)雜一些。由于它是一個(gè)抽象類(lèi),我們將在函數(shù)中使用匿名類(lèi)beforeEach來(lái)創(chuàng)建一個(gè)客戶端來(lái)使用該擴(kuò)展ApiClient。
請(qǐng)注意,我們也使用該Http::fake()方法。這會(huì)在Http外觀上創(chuàng)建模擬,我們可以對(duì)其進(jìn)行斷言并防止在測(cè)試中發(fā)出 API 請(qǐng)求。
<?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; }); });
對(duì)于測(cè)試,我們確認(rèn)在各種請(qǐng)求方法上正確設(shè)置了請(qǐng)求屬性。我們還確認(rèn)baseUrl和authorize方法被正確調(diào)用。為了做出這些斷言,我們使用的Http::assertSent方法需要一個(gè)帶有 a 的回調(diào)$request,我們可以對(duì)其進(jìn)行測(cè)試。請(qǐng)注意,我正在使用 PestPHP 期望,然后返回true. 我們可以只使用正常的比較并返回它,但是通過(guò)使用期望,當(dāng)測(cè)試失敗時(shí)我們會(huì)得到更清晰的錯(cuò)誤消息。閱讀這篇優(yōu)秀的文章以獲取更多信息。
GoogleBooksApiClient測(cè)試
測(cè)試與我們只想確保正確處理自定義實(shí)現(xiàn)細(xì)節(jié)的測(cè)試GoogleBooksApiClient類(lèi)似,例如設(shè)置基本 URL 并使用 API 密鑰添加查詢(xún)參數(shù)。ApiClient
另外,不是方法config中的助手beforeEach。通過(guò)使用幫助程序,我們可以為將在每個(gè)測(cè)試中使用的 Google 圖書(shū)服務(wù)配置設(shè)置測(cè)試值。文章來(lái)源:http://www.zghlxwxcb.cn/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; }); });
總結(jié)
在本文中,我們介紹了在 Laravel 中集成第三方 API 的一些有用步驟。通過(guò)使用這些簡(jiǎn)單的自定義類(lèi)以及外觀Http,我們可以確保所有集成功能相似,更易于測(cè)試,并且不需要任何項(xiàng)目依賴(lài)項(xiàng)。在后面的文章中,我將通過(guò)介紹 DTO、使用模擬響應(yīng)進(jìn)行測(cè)試以及使用 API 資源來(lái)擴(kuò)展這些集成技巧。文章來(lái)源地址http://www.zghlxwxcb.cn/article/692.html
到此這篇關(guān)于使用 Laravel 的 Http Facade(門(mén)面) 簡(jiǎn)化 API 集成的文章就介紹到這了,更多相關(guān)內(nèi)容可以在右上角搜索或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!