HTTP 用戶端

簡介

Laravel 為 Guzzle HTTP 用戶端提供了一個語意化的極簡 API,能讓我們快速建立外連 HTTP Request 來與其他 Web App 通訊。Laravel 的 Guzzle 包裝著重於各個常見的使用情境,並提供優秀的開發人員經驗(Developer Experience)

在開始前,先確保有將 Guzzle 套件安裝為專案的相依性套件。預設情況下,Laravel 已自動包含了這個相依性套件,但若你之前有將其移除,請使用 Composer 再安裝一次:

1composer require guzzlehttp/guzzle
1composer require guzzlehttp/guzzle

建立 Request

若要建立 Request,可以使用 Http Facade 提供的 headgetpostputpatchdelete 等方法。首先,我們先看看要查詢另一個 URL 的基礎 GET Request 怎麼建立:

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::get('http://example.com');
1use Illuminate\Support\Facades\Http;
2 
3$response = Http::get('http://example.com');

get 方法會回傳 Illuminate\Http\Client\Response 的實體,該實體提供了許多用來取得 Response 資訊的方法:

1$response->body() : string;
2$response->json($key = null, $default = null) : array|mixed;
3$response->object() : object;
4$response->collect($key = null) : Illuminate\Support\Collection;
5$response->status() : int;
6$response->successful() : bool;
7$response->redirect(): bool;
8$response->failed() : bool;
9$response->clientError() : bool;
10$response->header($header) : string;
11$response->headers() : array;
1$response->body() : string;
2$response->json($key = null, $default = null) : array|mixed;
3$response->object() : object;
4$response->collect($key = null) : Illuminate\Support\Collection;
5$response->status() : int;
6$response->successful() : bool;
7$response->redirect(): bool;
8$response->failed() : bool;
9$response->clientError() : bool;
10$response->header($header) : string;
11$response->headers() : array;

Illuminate\Http\Client\Response 物件也實作了 PHP 的 ArrayAccess 實體,能讓我們直接在 Response 上存取 JSON Response 資料:

1return Http::get('http://example.com/users/1')['name'];
1return Http::get('http://example.com/users/1')['name'];

除了上述所列的 Response 方法外,也可以使用下列方法來判斷 Response 是否有給定的狀態碼:

1$response->ok() : bool; // 200 OK
2$response->created() : bool; // 201 Created
3$response->accepted() : bool; // 202 Accepted
4$response->noContent() : bool; // 204 No Content
5$response->movedPermanently() : bool; // 301 Moved Permanently
6$response->found() : bool; // 302 Found
7$response->badRequest() : bool; // 400 Bad Request
8$response->unauthorized() : bool; // 401 Unauthorized
9$response->paymentRequired() : bool; // 402 Payment Required
10$response->forbidden() : bool; // 403 Forbidden
11$response->notFound() : bool; // 404 Not Found
12$response->requestTimeout() : bool; // 408 Request Timeout
13$response->conflict() : bool; // 409 Conflict
14$response->unprocessableEntity() : bool; // 422 Unprocessable Entity
15$response->tooManyRequests() : bool; // 429 Too Many Requests
16$response->serverError() : bool; // 500 Internal Server Error
1$response->ok() : bool; // 200 OK
2$response->created() : bool; // 201 Created
3$response->accepted() : bool; // 202 Accepted
4$response->noContent() : bool; // 204 No Content
5$response->movedPermanently() : bool; // 301 Moved Permanently
6$response->found() : bool; // 302 Found
7$response->badRequest() : bool; // 400 Bad Request
8$response->unauthorized() : bool; // 401 Unauthorized
9$response->paymentRequired() : bool; // 402 Payment Required
10$response->forbidden() : bool; // 403 Forbidden
11$response->notFound() : bool; // 404 Not Found
12$response->requestTimeout() : bool; // 408 Request Timeout
13$response->conflict() : bool; // 409 Conflict
14$response->unprocessableEntity() : bool; // 422 Unprocessable Entity
15$response->tooManyRequests() : bool; // 429 Too Many Requests
16$response->serverError() : bool; // 500 Internal Server Error

URI 樣板

在 HTTP Client 中,也可使用 URI 樣板規格 (URI Template Specification)來建立 Request URL。若要定義可由 URI 樣板展開的 URL 參數,請使用 withUrlParameters 方法:

1Http::withUrlParameters([
2 'endpoint' => 'https://laravel.com',
3 'page' => 'docs',
4 'version' => '9.x',
5 'topic' => 'validation',
6])->get('{+endpoint}/{page}/{version}/{topic}');
1Http::withUrlParameters([
2 'endpoint' => 'https://laravel.com',
3 'page' => 'docs',
4 'version' => '9.x',
5 'topic' => 'validation',
6])->get('{+endpoint}/{page}/{version}/{topic}');

傾印 Request

若想在送出 Request 前傾印連外 Request 並終止指令碼執行,可在 Request 定義的最前方加上 dd 方法:

1return Http::dd()->get('http://example.com');
1return Http::dd()->get('http://example.com');

Request 資料

當然,我們也很常使用 POSTPUTPATCH 等 Request 來在 Request 上傳送額外資料,所以這些方法接受資料陣列作為第二個引數。預設情況下,資料會使用 application/json Content Type(內容型別) 來傳送:

1use Illuminate\Support\Facades\Http;
2 
3$response = Http::post('http://example.com/users', [
4 'name' => 'Steve',
5 'role' => 'Network Administrator',
6]);
1use Illuminate\Support\Facades\Http;
2 
3$response = Http::post('http://example.com/users', [
4 'name' => 'Steve',
5 'role' => 'Network Administrator',
6]);

GET Request 查詢參數

在產生 GET Request 時,可以直接將查詢字串(Query String)加到 URL 上,或是傳入一組索引鍵 / 值配對的陣列作為 get 方法的第二個引數:

1$response = Http::get('http://example.com/users', [
2 'name' => 'Taylor',
3 'page' => 1,
4]);
1$response = Http::get('http://example.com/users', [
2 'name' => 'Taylor',
3 'page' => 1,
4]);

傳送 Form URL Encoded 的 Request

若想使用 application/x-www-form-urlencoded Content Type 來傳送資料的話,請在建立 Request 前呼叫 asForm 方法:

1$response = Http::asForm()->post('http://example.com/users', [
2 'name' => 'Sara',
3 'role' => 'Privacy Consultant',
4]);
1$response = Http::asForm()->post('http://example.com/users', [
2 'name' => 'Sara',
3 'role' => 'Privacy Consultant',
4]);

傳送原始 Request 內文

在建立 Request 時,若想提供原始 Request 內文(Raw Request Body),可使用 withBody 方法。可以在該方法的第二個引數上提供 Content Type:

1$response = Http::withBody(
2 base64_encode($photo), 'image/jpeg'
3)->post('http://example.com/photo');
1$response = Http::withBody(
2 base64_encode($photo), 'image/jpeg'
3)->post('http://example.com/photo');

Multi-Part 的 Request

若想使用 Multi-Part 的 Request 來傳送檔案的話,請在建立 Request 前呼叫 attach 方法。該方法接受檔案的欄位名稱、以及檔案的內容。若有需要,也可以提供第三個引數,該引數會被當作檔案名稱:

1$response = Http::attach(
2 'attachment', file_get_contents('photo.jpg'), 'photo.jpg'
3)->post('http://example.com/attachments');
1$response = Http::attach(
2 'attachment', file_get_contents('photo.jpg'), 'photo.jpg'
3)->post('http://example.com/attachments');

除了直接傳入檔案的原始內容外,也可以傳入一個 Stream Resource(串流資源)

1$photo = fopen('photo.jpg', 'r');
2 
3$response = Http::attach(
4 'attachment', $photo, 'photo.jpg'
5)->post('http://example.com/attachments');
1$photo = fopen('photo.jpg', 'r');
2 
3$response = Http::attach(
4 'attachment', $photo, 'photo.jpg'
5)->post('http://example.com/attachments');

標頭

可以使用 withHeaders 方法來將標頭(Header)加到 Request 上。withHeaders 方法接受一組索引鍵 / 值配對的陣列:

1$response = Http::withHeaders([
2 'X-First' => 'foo',
3 'X-Second' => 'bar'
4])->post('http://example.com/users', [
5 'name' => 'Taylor',
6]);
1$response = Http::withHeaders([
2 'X-First' => 'foo',
3 'X-Second' => 'bar'
4])->post('http://example.com/users', [
5 'name' => 'Taylor',
6]);

可以使用 accept 方法來指定你的程式預期所預期 Response 的 Content Type:

1$response = Http::accept('application/json')->get('http://example.com/users');
1$response = Http::accept('application/json')->get('http://example.com/users');

為了方便起見,可以使用 acceptJson 方法來快速指定要預期 Response 的 Content Type 是 application/json

1$response = Http::acceptJson()->get('http://example.com/users');
1$response = Http::acceptJson()->get('http://example.com/users');

身分驗證

可以使用 withBasicAuth 方法來指定使用 Basic 身分驗證的認證(Credential),或是使用 withDigestAuth 方法來指定 Digest 身分驗證的認證:

1// Basic 身份認證...
2$response = Http::withBasicAuth('[email protected]', 'secret')->post(/* ... */);
3 
4// Digest 身份認證...
5$response = Http::withDigestAuth('[email protected]', 'secret')->post(/* ... */);
1// Basic 身份認證...
2$response = Http::withBasicAuth('[email protected]', 'secret')->post(/* ... */);
3 
4// Digest 身份認證...
5$response = Http::withDigestAuth('[email protected]', 'secret')->post(/* ... */);

Bearer 權杖

若想快速在 Request 的 Authorization 標頭中加上 Bearer 權杖(Token),可使用 withToken 方法:

1$response = Http::withToken('token')->post(/* ... */);
1$response = Http::withToken('token')->post(/* ... */);

逾時

可使用 timeout 方法來為 Response 指定最多要等待的秒數:

1$response = Http::timeout(3)->get(/* ... */);
1$response = Http::timeout(3)->get(/* ... */);

當達到給定的逾時秒數後,會擲回 Illuminate\Http\Client\ConnectionException 實體。

可以使用 connectTimeout 方法來指定嘗試連線到伺服器時要等待的最大秒數:

1$response = Http::connectTimeout(3)->get(/* ... */);
1$response = Http::connectTimeout(3)->get(/* ... */);

重試

若想讓 HTTP 用戶端在發生用戶端錯誤或伺服器端錯誤時自動重試,可以使用 retry 方法。retry 方法接受該 Request 要重試的最大次數,以及每次重試間要等待多少毫秒:

1$response = Http::retry(3, 100)->post(/* ... */);
1$response = Http::retry(3, 100)->post(/* ... */);

若有需要,可以傳入第三個引數給 retry 方法。第三個引數應為一個 Callable,用來判斷是否要重試。舉例來說,我們可以判斷只在 Request 遇到 ConnectionException 時才重試:

1$response = Http::retry(3, 100, function ($exception, $request) {
2 return $exception instanceof ConnectionException;
3})->post(/* ... */);
1$response = Http::retry(3, 100, function ($exception, $request) {
2 return $exception instanceof ConnectionException;
3})->post(/* ... */);

若 Request 查詢失敗,我們可能會想在進行新嘗試前對 Request 做點修改。若要在重新嘗試前對 Request 做修改,我們只需要將提供 retry 方法的 Request 引數更改為 Callable 即可。舉例來說,在第一次嘗試回傳身份驗證錯誤時,我們可能會想以新的 Authorization Token 來重試該 Request:

1$response = Http::withToken($this->getToken())->retry(2, 0, function ($exception, $request) {
2 if (! $exception instanceof RequestException || $exception->response->status() !== 401) {
3 return false;
4 }
5 
6 $request->withToken($this->getNewToken());
7 
8 return true;
9})->post(/* ... */);
1$response = Http::withToken($this->getToken())->retry(2, 0, function ($exception, $request) {
2 if (! $exception instanceof RequestException || $exception->response->status() !== 401) {
3 return false;
4 }
5 
6 $request->withToken($this->getNewToken());
7 
8 return true;
9})->post(/* ... */);

若 Request 執行失敗,會擲回一個 Illuminate\Http\Client\RequestException 實體。若想禁用這個行為,可傳入 falsethrow 引數。當禁用擲回 Exception 時,會回傳所有重試中用戶端收到的最後一個 Response:

1$response = Http::retry(3, 100, throw: false)->post(/* ... */);
1$response = Http::retry(3, 100, throw: false)->post(/* ... */);
exclamation

若所有的 Request 都因為連線問題而失敗,即使 throw 引數設為 false,還是會擲回 Illuminate\Http\Client\ConnectionException

錯誤處理

與 Guzzle 預設的行為不同,Laravel 的 HTTP 用戶端在遇到用戶端錯誤或伺服器端錯誤時 (即,伺服器回傳 4XX5XX 等級的錯誤),不會擲回 Exception。我們可以使用 successfulclientErrorserverError 等方法來判斷是否遇到這類錯誤:

1// 判斷狀態碼是否 >= 200 且 < 300...
2$response->successful();
3 
4// 判斷狀態碼是否 >= 400...
5$response->failed();
6 
7// 判斷 Response 是否為 4XX 等級的狀態碼...
8$response->clientError();
9 
10// 判斷 Response 是否為 5XX 等級的狀態碼...
11$response->serverError();
12 
13// 若發生用戶端或伺服器段錯誤,馬上執行給定的回呼...
14$response->onError(callable $callback);
1// 判斷狀態碼是否 >= 200 且 < 300...
2$response->successful();
3 
4// 判斷狀態碼是否 >= 400...
5$response->failed();
6 
7// 判斷 Response 是否為 4XX 等級的狀態碼...
8$response->clientError();
9 
10// 判斷 Response 是否為 5XX 等級的狀態碼...
11$response->serverError();
12 
13// 若發生用戶端或伺服器段錯誤,馬上執行給定的回呼...
14$response->onError(callable $callback);

擲回 Exception

假設有個 Response 實體,而我們想在該 Response 的狀態碼為伺服器端或用戶端錯誤時擲回 Illuminate\Http\Client\RequestException,則可以使用 throwthrowIf 方法:

1$response = Http::post(/* ... */);
2 
3// 當發生 Client 端或 Server 端錯誤時擲回 Exception...
4$response->throw();
5 
6// 當發生錯誤且給定條件為 true 時擲回 Exception...
7$response->throwIf($condition);
8 
9// 當發生錯誤且給定閉包解析為 true 時擲回 Exception...
10$response->throwIf(fn ($response) => true);
11 
12// 當發生錯誤且給定條件為 false 時擲回 Exception...
13$response->throwUnless($condition);
14 
15// 當發生錯誤且給定閉包解析為 false 時擲回 Exception...
16$response->throwUnless(fn ($response) => false);
17 
18// 當 Response 會特定狀態碼時擲回 Exception...
19$response->throwIfStatus(403);
20 
21// 除非 Response 為特定狀態碼,否則擲回 Exception...
22$response->throwUnlessStatus(200);
23 
24return $response['user']['id'];
1$response = Http::post(/* ... */);
2 
3// 當發生 Client 端或 Server 端錯誤時擲回 Exception...
4$response->throw();
5 
6// 當發生錯誤且給定條件為 true 時擲回 Exception...
7$response->throwIf($condition);
8 
9// 當發生錯誤且給定閉包解析為 true 時擲回 Exception...
10$response->throwIf(fn ($response) => true);
11 
12// 當發生錯誤且給定條件為 false 時擲回 Exception...
13$response->throwUnless($condition);
14 
15// 當發生錯誤且給定閉包解析為 false 時擲回 Exception...
16$response->throwUnless(fn ($response) => false);
17 
18// 當 Response 會特定狀態碼時擲回 Exception...
19$response->throwIfStatus(403);
20 
21// 除非 Response 為特定狀態碼,否則擲回 Exception...
22$response->throwUnlessStatus(200);
23 
24return $response['user']['id'];

Illuminate\Http\Client\RequestException 實體有個 $response 公用屬性,我們可以使用該屬性來取得回傳的 Response。

如果沒有發生錯誤,throw 方法會回傳 Response 實體,能讓我們在 throw 方法後繼續串上其他操作:

1return Http::post(/* ... */)->throw()->json();
1return Http::post(/* ... */)->throw()->json();

若想在 Exception 被擲回前加上其他額外的邏輯,可傳入一個閉包給 throw 方法。叫用閉包後,就會自動擲回 Exception,因此我們不需要在閉包內重新擲回 Exception:

1return Http::post(/* ... */)->throw(function ($response, $e) {
2 //
3})->json();
1return Http::post(/* ... */)->throw(function ($response, $e) {
2 //
3})->json();

Guzzle Middleware

由於 Laravel 的 HTTP 用戶端使用 Guzzle,因此我們也可以使用 Guzzle 的 Middleware 功能來對修改連外 Request,或是檢查連入的 Response。若要修改連外的 Request,可使用 withMiddleware 方法來註冊 Guzzle Middleware,並搭配使用 Guzzle 的 mapRequest Middleware Factory:

1use GuzzleHttp\Middleware;
2use Illuminate\Support\Facades\Http;
3use Psr\Http\Message\RequestInterface;
4 
5$response = Http::withMiddleware(
6 Middleware::mapRequest(function (RequestInterface $request) {
7 $request = $request->withHeader('X-Example', 'Value');
8 
9 return $request;
10 })
11)->get('http://example.com');
1use GuzzleHttp\Middleware;
2use Illuminate\Support\Facades\Http;
3use Psr\Http\Message\RequestInterface;
4 
5$response = Http::withMiddleware(
6 Middleware::mapRequest(function (RequestInterface $request) {
7 $request = $request->withHeader('X-Example', 'Value');
8 
9 return $request;
10 })
11)->get('http://example.com');

類似地,我們也可以將 withMiddleware 方法與 Guzzle 的 mapResponse Middleware Factory 搭配使用來註冊用於檢查連入 HTTP Request 的 Middleware:

1use GuzzleHttp\Middleware;
2use Illuminate\Support\Facades\Http;
3use Psr\Http\Message\ResponseInterface;
4 
5$response = Http::withMiddleware(
6 Middleware::mapResponse(function (ResponseInterface $response) {
7 $header = $response->getHeader('X-Example');
8 
9 // ...
10 
11 return $response;
12 })
13)->get('http://example.com');
1use GuzzleHttp\Middleware;
2use Illuminate\Support\Facades\Http;
3use Psr\Http\Message\ResponseInterface;
4 
5$response = Http::withMiddleware(
6 Middleware::mapResponse(function (ResponseInterface $response) {
7 $header = $response->getHeader('X-Example');
8 
9 // ...
10 
11 return $response;
12 })
13)->get('http://example.com');

Guzzle 選項

我們可以使用 withOptions 方法來指定額外的 Guzzle Request 選項withOptions 方法接受一組索引鍵 / 值配對的陣列:

1$response = Http::withOptions([
2 'debug' => true,
3])->get('http://example.com/users');
1$response = Http::withOptions([
2 'debug' => true,
3])->get('http://example.com/users');

同時進行的 Request

有時候,我們可能會想同時進行多個 HTTP Request。換句話說,不是依序執行 Request,而是同時分派多個 Request。同時執行多個 Request 的話,在處理速度慢的 HTTP API 時就可以大幅提升效能。

所幸,我們只要使用 pool 方法就能達成。pool 方法接受一個閉包,該閉包會收到 Illuminate\Http\Client\Pool 實體,能讓我們輕鬆地將 Request 加到 Request Pool(請求集區) 以作分派:

1use Illuminate\Http\Client\Pool;
2use Illuminate\Support\Facades\Http;
3 
4$responses = Http::pool(fn (Pool $pool) => [
5 $pool->get('http://localhost/first'),
6 $pool->get('http://localhost/second'),
7 $pool->get('http://localhost/third'),
8]);
9 
10return $responses[0]->ok() &&
11 $responses[1]->ok() &&
12 $responses[2]->ok();
1use Illuminate\Http\Client\Pool;
2use Illuminate\Support\Facades\Http;
3 
4$responses = Http::pool(fn (Pool $pool) => [
5 $pool->get('http://localhost/first'),
6 $pool->get('http://localhost/second'),
7 $pool->get('http://localhost/third'),
8]);
9 
10return $responses[0]->ok() &&
11 $responses[1]->ok() &&
12 $responses[2]->ok();

就像這樣,我們可以依據加入 Pool 的順序來存取每個 Response 實體。若有需要的話,也可以使用 as 方法來為 Request 命名,好讓我們能使用名稱來存取對應的 Response:

1use Illuminate\Http\Client\Pool;
2use Illuminate\Support\Facades\Http;
3 
4$responses = Http::pool(fn (Pool $pool) => [
5 $pool->as('first')->get('http://localhost/first'),
6 $pool->as('second')->get('http://localhost/second'),
7 $pool->as('third')->get('http://localhost/third'),
8]);
9 
10return $responses['first']->ok();
1use Illuminate\Http\Client\Pool;
2use Illuminate\Support\Facades\Http;
3 
4$responses = Http::pool(fn (Pool $pool) => [
5 $pool->as('first')->get('http://localhost/first'),
6 $pool->as('second')->get('http://localhost/second'),
7 $pool->as('third')->get('http://localhost/third'),
8]);
9 
10return $responses['first']->ok();

Macro

Laravel HTTP 用戶端支援定義「Macro(巨集)」。通過 Macro,我們就能通過一些流暢且語義化的機制來在專案中為一些服務設定常用的 Request 路徑與標頭。若要開始使用 Macro,我們可以在專案的 App\Providers\AppServiceProviderboot 方法中定義 Macro:

1use Illuminate\Support\Facades\Http;
2 
3/**
4 * Bootstrap any application services.
5 *
6 * @return void
7 */
8public function boot()
9{
10 Http::macro('github', function () {
11 return Http::withHeaders([
12 'X-Example' => 'example',
13 ])->baseUrl('https://github.com');
14 });
15}
1use Illuminate\Support\Facades\Http;
2 
3/**
4 * Bootstrap any application services.
5 *
6 * @return void
7 */
8public function boot()
9{
10 Http::macro('github', function () {
11 return Http::withHeaders([
12 'X-Example' => 'example',
13 ])->baseUrl('https://github.com');
14 });
15}

設定好 Macro 後,就可以在任何地方叫用這個 Macro,以使用指定的設定來建立 Request:

1$response = Http::github()->get('/');
1$response = Http::github()->get('/');

測試

許多 Laravel 的服務都提供了能讓我們輕鬆撰寫測試的功能,而 Laravel 的 HTTP 用戶端也不例外。Http Facade 的 fake 方法能讓我們指定 HTTP 用戶端在建立 Request 後回傳一組虛擬的 Response。

模擬(Fake) Response

舉例來說,若要讓 HTTP 用戶端為每個 Request 回傳 200 狀態碼的空 Response,可呼叫 fake 方法,然後不傳入任何引數:

1use Illuminate\Support\Facades\Http;
2 
3Http::fake();
4 
5$response = Http::post(/* ... */);
1use Illuminate\Support\Facades\Http;
2 
3Http::fake();
4 
5$response = Http::post(/* ... */);

模擬執行 URL

或者,我們也可以傳入一組陣列給 fake 方法。該陣列的索引鍵代表要模擬的 URL,對應的值則為 Response。可使用 * 字元來當作萬用字元。當 Request 的 URL 不在模擬列表內時,就會被實際執行。可以使用 Http Facade 的 response 方法來為這些Endpoint(端點)建立虛擬的 Response:

1Http::fake([
2 // 為 GitHub Endpoint 模擬一個 JSON Response...
3 'github.com/*' => Http::response(['foo' => 'bar'], 200, $headers),
4 
5 // 為 Google Endpoint 模擬一個字串的 Response...
6 'google.com/*' => Http::response('Hello World', 200, $headers),
7]);
1Http::fake([
2 // 為 GitHub Endpoint 模擬一個 JSON Response...
3 'github.com/*' => Http::response(['foo' => 'bar'], 200, $headers),
4 
5 // 為 Google Endpoint 模擬一個字串的 Response...
6 'google.com/*' => Http::response('Hello World', 200, $headers),
7]);

若想為所有不符合的 URL 建立一個遞補用 URL 規則,只要使用單一 * 字元即可:

1Http::fake([
2 // 為 GitHub Endpoint 模擬一個 JSON Response...
3 'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']),
4 
5 // 為所有其他的 Endpoint 模擬一個字串的 Response...
6 '*' => Http::response('Hello World', 200, ['Headers']),
7]);
1Http::fake([
2 // 為 GitHub Endpoint 模擬一個 JSON Response...
3 'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']),
4 
5 // 為所有其他的 Endpoint 模擬一個字串的 Response...
6 '*' => Http::response('Hello World', 200, ['Headers']),
7]);

模擬 Response 序列

有時候我們需要讓單一 URL 以固定的順序回傳一系列模擬的 Response。我們可以使用 Http::sequence 方法來建立 Request:

1Http::fake([
2 // 為 GitHub Endpoint 模擬一系列的 Response...
3 'github.com/*' => Http::sequence()
4 ->push('Hello World', 200)
5 ->push(['foo' => 'bar'], 200)
6 ->pushStatus(404),
7]);
1Http::fake([
2 // 為 GitHub Endpoint 模擬一系列的 Response...
3 'github.com/*' => Http::sequence()
4 ->push('Hello World', 200)
5 ->push(['foo' => 'bar'], 200)
6 ->pushStatus(404),
7]);

用完 Response 序列內的所有 Response 後,若之後又建立新的 Request,就會導致 Response 序列擲回 Exception。若想指定當序列為空時要回傳的預設 Response,可使用 whenEmpty 方法:

1Http::fake([
2 // 為 GitHub Endpoint 模擬一系列的 Response...
3 'github.com/*' => Http::sequence()
4 ->push('Hello World', 200)
5 ->push(['foo' => 'bar'], 200)
6 ->whenEmpty(Http::response()),
7]);
1Http::fake([
2 // 為 GitHub Endpoint 模擬一系列的 Response...
3 'github.com/*' => Http::sequence()
4 ->push('Hello World', 200)
5 ->push(['foo' => 'bar'], 200)
6 ->whenEmpty(Http::response()),
7]);

若想模擬一系列的 Response,但又不想指定要模擬的特定 URL 格式,可使用 Http::fakeSequence 方法:

1Http::fakeSequence()
2 ->push('Hello World', 200)
3 ->whenEmpty(Http::response());
1Http::fakeSequence()
2 ->push('Hello World', 200)
3 ->whenEmpty(Http::response());

模擬回呼

若某些 Endpoint 需要使用比較複雜的邏輯來判斷要回傳什麼 Response 的話,可傳入一個閉包給 fake 方法。該閉包會收到一組 Illuminate\Http\Client\Request 的實體,而該閉包必須回傳 Response 實體。在這個閉包內,我們就可以任意加上邏輯來判斷要回傳什麼類型的 Response:

1use Illuminate\Http\Client\Request;
2 
3Http::fake(function (Request $request) {
4 return Http::response('Hello World', 200);
5});
1use Illuminate\Http\Client\Request;
2 
3Http::fake(function (Request $request) {
4 return Http::response('Hello World', 200);
5});

避免漏掉的 Request

若要確保在個別測試或整個測試套件中,所有使用 HTTP 用戶端的 Request 都有被 Fake 到,則可以使用 preventStrayRequests 方法。呼叫該方法後,若有任何找不到對應 Fake Response 的 Request,就不會產生實際的 HTTP Request,而會擲回 Exception:

1use Illuminate\Support\Facades\Http;
2 
3Http::preventStrayRequests();
4 
5Http::fake([
6 'github.com/*' => Http::response('ok'),
7]);
8 
9// 回傳「ok」Response...
10Http::get('https://github.com/laravel/framework');
11 
12// 擲回 Exception...
13Http::get('https://laravel.com');
1use Illuminate\Support\Facades\Http;
2 
3Http::preventStrayRequests();
4 
5Http::fake([
6 'github.com/*' => Http::response('ok'),
7]);
8 
9// 回傳「ok」Response...
10Http::get('https://github.com/laravel/framework');
11 
12// 擲回 Exception...
13Http::get('https://laravel.com');

檢查 Request

在模擬 Response 時,有時候我們會需要檢查用戶端收到的 Request,以確保程式有傳送正確的資料。可以在呼叫 Http::fake 之前先呼叫 Http::assertSent 方法來檢查。

assertSent 方法接受一組閉包,該閉包會收到 Illuminate\Http\Client\Request 的實體,而該閉包應回傳用來表示 Request 是否符合預期的布林值。若要讓測試通過,提供的 Request 中就必須至少有一個是符合給定預期條件的:

1use Illuminate\Http\Client\Request;
2use Illuminate\Support\Facades\Http;
3 
4Http::fake();
5 
6Http::withHeaders([
7 'X-First' => 'foo',
8])->post('http://example.com/users', [
9 'name' => 'Taylor',
10 'role' => 'Developer',
11]);
12 
13Http::assertSent(function (Request $request) {
14 return $request->hasHeader('X-First', 'foo') &&
15 $request->url() == 'http://example.com/users' &&
16 $request['name'] == 'Taylor' &&
17 $request['role'] == 'Developer';
18});
1use Illuminate\Http\Client\Request;
2use Illuminate\Support\Facades\Http;
3 
4Http::fake();
5 
6Http::withHeaders([
7 'X-First' => 'foo',
8])->post('http://example.com/users', [
9 'name' => 'Taylor',
10 'role' => 'Developer',
11]);
12 
13Http::assertSent(function (Request $request) {
14 return $request->hasHeader('X-First', 'foo') &&
15 $request->url() == 'http://example.com/users' &&
16 $request['name'] == 'Taylor' &&
17 $request['role'] == 'Developer';
18});

若有需要,可以使用 assertNotSent 方法來判斷特定 Request 是否未被送出:

1use Illuminate\Http\Client\Request;
2use Illuminate\Support\Facades\Http;
3 
4Http::fake();
5 
6Http::post('http://example.com/users', [
7 'name' => 'Taylor',
8 'role' => 'Developer',
9]);
10 
11Http::assertNotSent(function (Request $request) {
12 return $request->url() === 'http://example.com/posts';
13});
1use Illuminate\Http\Client\Request;
2use Illuminate\Support\Facades\Http;
3 
4Http::fake();
5 
6Http::post('http://example.com/users', [
7 'name' => 'Taylor',
8 'role' => 'Developer',
9]);
10 
11Http::assertNotSent(function (Request $request) {
12 return $request->url() === 'http://example.com/posts';
13});

可以使用 assertSentCount 方法來判斷在測試時「送出」了多少個 Request:

1Http::fake();
2 
3Http::assertSentCount(5);
1Http::fake();
2 
3Http::assertSentCount(5);

或者,也可以使用 assertNothingSent 方法來判斷在測試時是否未送出任何 Request:

1Http::fake();
2 
3Http::assertNothingSent();
1Http::fake();
2 
3Http::assertNothingSent();

記錄 Request 或 Response

可以使用 recorded 方法來取得所有的 Request 與其對應的 Response。recorded 方法會回傳一組陣列的 Collection,其內容為 Illuminate\Http\Client\RequestIlluminate\Http\Client\Response 的實體:

1Http::fake([
2 'https://laravel.com' => Http::response(status: 500),
3 'https://nova.laravel.com/' => Http::response(),
4]);
5 
6Http::get('https://laravel.com');
7Http::get('https://nova.laravel.com/');
8 
9$recorded = Http::recorded();
10 
11[$request, $response] = $recorded[0];
1Http::fake([
2 'https://laravel.com' => Http::response(status: 500),
3 'https://nova.laravel.com/' => Http::response(),
4]);
5 
6Http::get('https://laravel.com');
7Http::get('https://nova.laravel.com/');
8 
9$recorded = Http::recorded();
10 
11[$request, $response] = $recorded[0];

此外,也可傳入閉包給 recorded 方法,該閉包會收到 Illuminate\Http\Client\RequestIlluminate\Http\Client\Response 的實體。可以傳入閉包來依據需求過濾 Request/Response 配對:

1use Illuminate\Http\Client\Request;
2use Illuminate\Http\Client\Response;
3 
4Http::fake([
5 'https://laravel.com' => Http::response(status: 500),
6 'https://nova.laravel.com/' => Http::response(),
7]);
8 
9Http::get('https://laravel.com');
10Http::get('https://nova.laravel.com/');
11 
12$recorded = Http::recorded(function (Request $request, Response $response) {
13 return $request->url() !== 'https://laravel.com' &&
14 $response->successful();
15});
1use Illuminate\Http\Client\Request;
2use Illuminate\Http\Client\Response;
3 
4Http::fake([
5 'https://laravel.com' => Http::response(status: 500),
6 'https://nova.laravel.com/' => Http::response(),
7]);
8 
9Http::get('https://laravel.com');
10Http::get('https://nova.laravel.com/');
11 
12$recorded = Http::recorded(function (Request $request, Response $response) {
13 return $request->url() !== 'https://laravel.com' &&
14 $response->successful();
15});

事件

在傳送 HTTP Request 的過程中,Laravel 會觸發三個事件。在送出 Request 前會觸發 RequestSending 事件,而給定 Request 收到 Response 後會觸發 ResponseReceived 事件。若給定的 Request 未收到 Response,會觸發 ConnectionFailed 事件。

RequestSendingConnectionFailed 事件都有一個 $request 共用屬性,可以通過這個屬性來取得 Illuminate\Http\Client\Request 實體。而 ResponseReceived 事件中也有一個 $request 公開屬性,以及一個可用來取得 Illuminate\Http\Client\Response 實體的 $response 公開屬性。可以在 App\Providers\EventServiceProvider Service Provider 中為這些 Event 註冊 Listener:

1/**
2 * The event listener mappings for the application.
3 *
4 * @var array
5 */
6protected $listen = [
7 'Illuminate\Http\Client\Events\RequestSending' => [
8 'App\Listeners\LogRequestSending',
9 ],
10 'Illuminate\Http\Client\Events\ResponseReceived' => [
11 'App\Listeners\LogResponseReceived',
12 ],
13 'Illuminate\Http\Client\Events\ConnectionFailed' => [
14 'App\Listeners\LogConnectionFailed',
15 ],
16];
1/**
2 * The event listener mappings for the application.
3 *
4 * @var array
5 */
6protected $listen = [
7 'Illuminate\Http\Client\Events\RequestSending' => [
8 'App\Listeners\LogRequestSending',
9 ],
10 'Illuminate\Http\Client\Events\ResponseReceived' => [
11 'App\Listeners\LogResponseReceived',
12 ],
13 'Illuminate\Http\Client\Events\ConnectionFailed' => [
14 'App\Listeners\LogConnectionFailed',
15 ],
16];
翻譯進度
100% 已翻譯
更新時間:
2023年7月22日 下午3:09:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.