HTTP 用戶端
簡介
Laravel 為 Guzzle HTTP 用戶端提供了一個語意化的極簡 API,能讓我們快速建立外連 HTTP Request 來與其他 Web App 通訊。Laravel 的 Guzzle 包裝著重於各個常見的使用情境,並提供優秀的開發人員經驗。
在開始前,先確保有將 Guzzle 套件安裝為專案的相依性套件。預設情況下,Laravel 已自動包含了這個相依性套件,但若你之前有將其移除,請使用 Composer 再安裝一次:
1composer require guzzlehttp/guzzle
1composer require guzzlehttp/guzzle
建立 Request
若要建立 Request,可以使用 Http
Facade 提供的 head
、get
、post
、put
、patch
、delete
等方法。首先,我們先看看要查詢另一個 URL 的基礎 GET
Request 怎麼建立:
1use Illuminate\Support\Facades\Http;23$response = Http::get('http://example.com');
1use Illuminate\Support\Facades\Http;23$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 OK2$response->created() : bool; // 201 Created3$response->accepted() : bool; // 202 Accepted4$response->noContent() : bool; // 204 No Content5$response->movedPermanently() : bool; // 301 Moved Permanently6$response->found() : bool; // 302 Found7$response->badRequest() : bool; // 400 Bad Request8$response->unauthorized() : bool; // 401 Unauthorized9$response->paymentRequired() : bool; // 402 Payment Required10$response->forbidden() : bool; // 403 Forbidden11$response->notFound() : bool; // 404 Not Found12$response->requestTimeout() : bool; // 408 Request Timeout13$response->conflict() : bool; // 409 Conflict14$response->unprocessableEntity() : bool; // 422 Unprocessable Entity15$response->tooManyRequests() : bool; // 429 Too Many Requests16$response->serverError() : bool; // 500 Internal Server Error
1$response->ok() : bool; // 200 OK2$response->created() : bool; // 201 Created3$response->accepted() : bool; // 202 Accepted4$response->noContent() : bool; // 204 No Content5$response->movedPermanently() : bool; // 301 Moved Permanently6$response->found() : bool; // 302 Found7$response->badRequest() : bool; // 400 Bad Request8$response->unauthorized() : bool; // 401 Unauthorized9$response->paymentRequired() : bool; // 402 Payment Required10$response->forbidden() : bool; // 403 Forbidden11$response->notFound() : bool; // 404 Not Found12$response->requestTimeout() : bool; // 408 Request Timeout13$response->conflict() : bool; // 409 Conflict14$response->unprocessableEntity() : bool; // 422 Unprocessable Entity15$response->tooManyRequests() : bool; // 429 Too Many Requests16$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 資料
當然,我們也很常使用 POST
、PUT
、PATCH
等 Request 來在 Request 上傳送額外資料,所以這些方法接受資料陣列作為第二個引數。預設情況下,資料會使用 application/json
Content Type 來傳送:
1use Illuminate\Support\Facades\Http;23$response = Http::post('http://example.com/users', [4 'name' => 'Steve',5 'role' => 'Network Administrator',6]);
1use Illuminate\Support\Facades\Http;23$response = Http::post('http://example.com/users', [4 'name' => 'Steve',5 'role' => 'Network Administrator',6]);
GET Request 查詢參數
在產生 GET
Request 時,可以直接將查詢字串加到 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]);
或者,可使用 withQueryParameters
方法:
1Http::retry(3, 100)->withQueryParameters([2 'name' => 'Taylor',3 'page' => 1,4])->get('http://example.com/users')
1Http::retry(3, 100)->withQueryParameters([2 'name' => 'Taylor',3 'page' => 1,4])->get('http://example.com/users')
傳送 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 內文,可使用 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');23$response = Http::attach(4 'attachment', $photo, 'photo.jpg'5)->post('http://example.com/attachments');
1$photo = fopen('photo.jpg', 'r');23$response = Http::attach(4 'attachment', $photo, 'photo.jpg'5)->post('http://example.com/attachments');
標頭
可以使用 withHeaders
方法來將標頭加到 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');
withHeaders
方法會將新的 Header 合併到該 Request 中已存在的 Header。若有需要,可使用 replaceHeaders
方法來講整個 Header 取代掉:
1$response = Http::withHeaders([2 'X-Original' => 'foo',3])->replaceHeaders([4 'X-Replacement' => 'bar',5])->post('http://example.com/users', [6 'name' => 'Taylor',7]);
1$response = Http::withHeaders([2 'X-Original' => 'foo',3])->replaceHeaders([4 'X-Replacement' => 'bar',5])->post('http://example.com/users', [6 'name' => 'Taylor',7]);
身分驗證
可以使用 withBasicAuth
方法來指定使用 Basic 身分驗證的認證,或是使用 withDigestAuth
方法來指定 Digest 身分驗證的認證:
1// Basic 身份認證...34// Digest 身份認證...
1// Basic 身份認證...34// Digest 身份認證...
Bearer 權杖
若想快速在 Request 的 Authorization
標頭中加上 Bearer 權杖,可使用 withToken
方法:
1$response = Http::withToken('token')->post(/* ... */);
1$response = Http::withToken('token')->post(/* ... */);
逾時
timeout
方法可用來指定該 Response 最長應等待多久。預設情況下,HTTP Client 會在 30 秒後逾時:
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
時才重試:
1use Exception;2use Illuminate\Http\Client\PendingRequest;34$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {5 return $exception instanceof ConnectionException;6})->post(/* ... */);
1use Exception;2use Illuminate\Http\Client\PendingRequest;34$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {5 return $exception instanceof ConnectionException;6})->post(/* ... */);
若 Request 查詢失敗,我們可能會想在進行新嘗試前對 Request 做點修改。若要在重新嘗試前對 Request 做修改,我們只需要將提供 retry
方法的 Request 引數更改為 Callable 即可。舉例來說,在第一次嘗試回傳身份驗證錯誤時,我們可能會想以新的 Authorization Token 來重試該 Request:
1use Exception;2use Illuminate\Http\Client\PendingRequest;3use Illuminate\Http\Client\RequestException;45$response = Http::withToken($this->getToken())->retry(2, 0, function (Exception $exception, PendingRequest $request) {6 if (! $exception instanceof RequestException || $exception->response->status() !== 401) {7 return false;8 }910 $request->withToken($this->getNewToken());1112 return true;13})->post(/* ... */);
1use Exception;2use Illuminate\Http\Client\PendingRequest;3use Illuminate\Http\Client\RequestException;45$response = Http::withToken($this->getToken())->retry(2, 0, function (Exception $exception, PendingRequest $request) {6 if (! $exception instanceof RequestException || $exception->response->status() !== 401) {7 return false;8 }910 $request->withToken($this->getNewToken());1112 return true;13})->post(/* ... */);
若 Request 執行失敗,會擲回一個 Illuminate\Http\Client\RequestException
實體。若想禁用這個行為,可傳入 false
給 throw
引數。當禁用擲回 Exception 時,會回傳所有重試中用戶端收到的最後一個 Response:
1$response = Http::retry(3, 100, throw: false)->post(/* ... */);
1$response = Http::retry(3, 100, throw: false)->post(/* ... */);
若所有的 Request 都因為連線問題而失敗,即使 throw
引數設為 false
,還是會擲回 Illuminate\Http\Client\ConnectionException
。
錯誤處理
與 Guzzle 預設的行為不同,Laravel 的 HTTP 用戶端在遇到用戶端錯誤或伺服器端錯誤時 (即,伺服器回傳 4XX
與 5XX
等級的錯誤),不會擲回 Exception。我們可以使用 successful
、clientError
、serverError
等方法來判斷是否遇到這類錯誤:
1// 判斷狀態碼是否 >= 200 且 < 300...2$response->successful();34// 判斷狀態碼是否 >= 400...5$response->failed();67// 判斷 Response 是否為 4XX 等級的狀態碼...8$response->clientError();910// 判斷 Response 是否為 5XX 等級的狀態碼...11$response->serverError();1213// 若發生用戶端或伺服器段錯誤,馬上執行給定的回呼...14$response->onError(callable $callback);
1// 判斷狀態碼是否 >= 200 且 < 300...2$response->successful();34// 判斷狀態碼是否 >= 400...5$response->failed();67// 判斷 Response 是否為 4XX 等級的狀態碼...8$response->clientError();910// 判斷 Response 是否為 5XX 等級的狀態碼...11$response->serverError();1213// 若發生用戶端或伺服器段錯誤,馬上執行給定的回呼...14$response->onError(callable $callback);
擲回 Exception
假設有個 Response 實體,而我們想在該 Response 的狀態碼為伺服器端或用戶端錯誤時擲回 Illuminate\Http\Client\RequestException
,則可以使用 throw
或 throwIf
方法:
1use Illuminate\Http\Client\Response;23$response = Http::post(/* ... */);45// 當發生 Client 端或 Server 端錯誤時擲回 Exception...6$response->throw();78// 當發生錯誤且給定條件為 true 時擲回 Exception...9$response->throwIf($condition);1011// 當發生錯誤且給定閉包解析為 true 時擲回 Exception...12$response->throwIf(fn (Response $response) => true);1314// 當發生錯誤且給定條件為 false 時擲回 Exception...15$response->throwUnless($condition);1617// 當發生錯誤且給定閉包解析為 false 時擲回 Exception...18$response->throwUnless(fn (Response $response) => false);1920// 當 Response 會特定狀態碼時擲回 Exception...21$response->throwIfStatus(403);2223// 除非 Response 為特定狀態碼,否則擲回 Exception...24$response->throwUnlessStatus(200);2526return $response['user']['id'];
1use Illuminate\Http\Client\Response;23$response = Http::post(/* ... */);45// 當發生 Client 端或 Server 端錯誤時擲回 Exception...6$response->throw();78// 當發生錯誤且給定條件為 true 時擲回 Exception...9$response->throwIf($condition);1011// 當發生錯誤且給定閉包解析為 true 時擲回 Exception...12$response->throwIf(fn (Response $response) => true);1314// 當發生錯誤且給定條件為 false 時擲回 Exception...15$response->throwUnless($condition);1617// 當發生錯誤且給定閉包解析為 false 時擲回 Exception...18$response->throwUnless(fn (Response $response) => false);1920// 當 Response 會特定狀態碼時擲回 Exception...21$response->throwIfStatus(403);2223// 除非 Response 為特定狀態碼,否則擲回 Exception...24$response->throwUnlessStatus(200);2526return $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:
1use Illuminate\Http\Client\Response;2use Illuminate\Http\Client\RequestException;34return Http::post(/* ... */)->throw(function (Response $response, RequestException $e) {5 // ...6})->json();
1use Illuminate\Http\Client\Response;2use Illuminate\Http\Client\RequestException;34return Http::post(/* ... */)->throw(function (Response $response, RequestException $e) {5 // ...6})->json();
Guzzle Middleware
由於 Laravel 的 HTTP 用戶端使用 Guzzle,因此我們也可以使用 Guzzle 的 Middleware 功能來對修改連外 Request,或是檢查連入的 Response。若要修改連外的 Request,可使用 withRequestMiddleware
方法來註冊 Guzzle Middleware:
1use Illuminate\Support\Facades\Http;2use Psr\Http\Message\RequestInterface;34$response = Http::withRequestMiddleware(5 function (RequestInterface $request) {6 return $request->withHeader('X-Example', 'Value');7 }8)->get('http://example.com');
1use Illuminate\Support\Facades\Http;2use Psr\Http\Message\RequestInterface;34$response = Http::withRequestMiddleware(5 function (RequestInterface $request) {6 return $request->withHeader('X-Example', 'Value');7 }8)->get('http://example.com');
類似地,我們也可以使用 withResponseMiddleware
方法來註冊 Middleware 以檢查連入的 HTTP Response:
1use Illuminate\Support\Facades\Http;2use Psr\Http\Message\ResponseInterface;34$response = Http::withResponseMiddleware(5 function (ResponseInterface $response) {6 $header = $response->getHeader('X-Example');78 // ...910 return $response;11 }12)->get('http://example.com');
1use Illuminate\Support\Facades\Http;2use Psr\Http\Message\ResponseInterface;34$response = Http::withResponseMiddleware(5 function (ResponseInterface $response) {6 $header = $response->getHeader('X-Example');78 // ...910 return $response;11 }12)->get('http://example.com');
全域 Middleware
有時候,我們會想註冊套用到每個連外 Request 與連入 Response 的 Middleware。這時候,我們可以使用 globalRequestMiddleware
與 globalResponseMiddleware
方法。一般來說,這些方法應在專案的 AppServiceProvider
中 boot
方法內呼叫:
1use Illuminate\Support\Facades\Http;23Http::globalRequestMiddleware(fn ($request) => $request->withHeader(4 'User-Agent', 'Example Application/1.0'5));67Http::globalResponseMiddleware(fn ($response) => $response->withHeader(8 'X-Finished-At', now()->toDateTimeString()9));
1use Illuminate\Support\Facades\Http;23Http::globalRequestMiddleware(fn ($request) => $request->withHeader(4 'User-Agent', 'Example Application/1.0'5));67Http::globalResponseMiddleware(fn ($response) => $response->withHeader(8 'X-Finished-At', now()->toDateTimeString()9));
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;34$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]);910return $responses[0]->ok() &&11 $responses[1]->ok() &&12 $responses[2]->ok();
1use Illuminate\Http\Client\Pool;2use Illuminate\Support\Facades\Http;34$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]);910return $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;34$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]);910return $responses['first']->ok();
1use Illuminate\Http\Client\Pool;2use Illuminate\Support\Facades\Http;34$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]);910return $responses['first']->ok();
自定同時進行的 Request
pool
方法不能與其他如 withHeaders
或 middleware
等 HTTP Client 方法一起串連使用。若想講自定 Header 或 Middleware 套用到放在 Pool 中的 Request,則必須將這些選項設定到 Pool 中的各個 Request:
1use Illuminate\Http\Client\Pool;2use Illuminate\Support\Facades\Http;34$headers = [5 'X-Example' => 'example',6];78$responses = Http::pool(fn (Pool $pool) => [9 $pool->withHeaders($headers)->get('http://laravel.test/test'),10 $pool->withHeaders($headers)->get('http://laravel.test/test'),11 $pool->withHeaders($headers)->get('http://laravel.test/test'),12]);
1use Illuminate\Http\Client\Pool;2use Illuminate\Support\Facades\Http;34$headers = [5 'X-Example' => 'example',6];78$responses = Http::pool(fn (Pool $pool) => [9 $pool->withHeaders($headers)->get('http://laravel.test/test'),10 $pool->withHeaders($headers)->get('http://laravel.test/test'),11 $pool->withHeaders($headers)->get('http://laravel.test/test'),12]);
Macro
Laravel HTTP 用戶端支援定義「Macro」。通過 Macro,我們就能通過一些流暢且語義化的機制來在專案中為一些服務設定常用的 Request 路徑與標頭。若要開始使用 Macro,我們可以在專案的 App\Providers\AppServiceProvider
內 boot
方法中定義 Macro:
1use Illuminate\Support\Facades\Http;23/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Http::macro('github', function () {9 return Http::withHeaders([10 'X-Example' => 'example',11 ])->baseUrl('https://github.com');12 });13}
1use Illuminate\Support\Facades\Http;23/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Http::macro('github', function () {9 return Http::withHeaders([10 'X-Example' => 'example',11 ])->baseUrl('https://github.com');12 });13}
設定好 Macro 後,就可以在任何地方叫用這個 Macro,以使用指定的設定來建立 Request:
1$response = Http::github()->get('/');
1$response = Http::github()->get('/');
測試
許多 Laravel 的服務都提供了能讓我們輕鬆撰寫測試的功能,而 Laravel 的 HTTP 用戶端也不例外。Http
Facade 的 fake
方法能讓我們指定 HTTP 用戶端在建立 Request 後回傳一組虛擬的 Response。
模擬 Response
舉例來說,若要讓 HTTP 用戶端為每個 Request 回傳 200
狀態碼的空 Response,可呼叫 fake
方法,然後不傳入任何引數:
1use Illuminate\Support\Facades\Http;23Http::fake();45$response = Http::post(/* ... */);
1use Illuminate\Support\Facades\Http;23Http::fake();45$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),45 // 為 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),45 // 為 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']),45 // 為所有其他的 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']),45 // 為所有其他的 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;23Http::fake(function (Request $request) {4 return Http::response('Hello World', 200);5});
1use Illuminate\Http\Client\Request;23Http::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;23Http::preventStrayRequests();45Http::fake([6 'github.com/*' => Http::response('ok'),7]);89// 回傳「ok」Response...10Http::get('https://github.com/laravel/framework');1112// 擲回 Exception...13Http::get('https://laravel.com');
1use Illuminate\Support\Facades\Http;23Http::preventStrayRequests();45Http::fake([6 'github.com/*' => Http::response('ok'),7]);89// 回傳「ok」Response...10Http::get('https://github.com/laravel/framework');1112// 擲回 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;34Http::fake();56Http::withHeaders([7 'X-First' => 'foo',8])->post('http://example.com/users', [9 'name' => 'Taylor',10 'role' => 'Developer',11]);1213Http::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;34Http::fake();56Http::withHeaders([7 'X-First' => 'foo',8])->post('http://example.com/users', [9 'name' => 'Taylor',10 'role' => 'Developer',11]);1213Http::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;34Http::fake();56Http::post('http://example.com/users', [7 'name' => 'Taylor',8 'role' => 'Developer',9]);1011Http::assertNotSent(function (Request $request) {12 return $request->url() === 'http://example.com/posts';13});
1use Illuminate\Http\Client\Request;2use Illuminate\Support\Facades\Http;34Http::fake();56Http::post('http://example.com/users', [7 'name' => 'Taylor',8 'role' => 'Developer',9]);1011Http::assertNotSent(function (Request $request) {12 return $request->url() === 'http://example.com/posts';13});
可以使用 assertSentCount
方法來判斷在測試時「送出」了多少個 Request:
1Http::fake();23Http::assertSentCount(5);
1Http::fake();23Http::assertSentCount(5);
或者,也可以使用 assertNothingSent
方法來判斷在測試時是否未送出任何 Request:
1Http::fake();23Http::assertNothingSent();
1Http::fake();23Http::assertNothingSent();
記錄 Request 或 Response
可以使用 recorded
方法來取得所有的 Request 與其對應的 Response。recorded
方法會回傳一組陣列的 Collection,其內容為 Illuminate\Http\Client\Request
與 Illuminate\Http\Client\Response
的實體:
1Http::fake([2 'https://laravel.com' => Http::response(status: 500),3 'https://nova.laravel.com/' => Http::response(),4]);56Http::get('https://laravel.com');7Http::get('https://nova.laravel.com/');89$recorded = Http::recorded();1011[$request, $response] = $recorded[0];
1Http::fake([2 'https://laravel.com' => Http::response(status: 500),3 'https://nova.laravel.com/' => Http::response(),4]);56Http::get('https://laravel.com');7Http::get('https://nova.laravel.com/');89$recorded = Http::recorded();1011[$request, $response] = $recorded[0];
此外,也可傳入閉包給 recorded
方法,該閉包會收到 Illuminate\Http\Client\Request
與 Illuminate\Http\Client\Response
的實體。可以傳入閉包來依據需求過濾 Request/Response 配對:
1use Illuminate\Http\Client\Request;2use Illuminate\Http\Client\Response;34Http::fake([5 'https://laravel.com' => Http::response(status: 500),6 'https://nova.laravel.com/' => Http::response(),7]);89Http::get('https://laravel.com');10Http::get('https://nova.laravel.com/');1112$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;34Http::fake([5 'https://laravel.com' => Http::response(status: 500),6 'https://nova.laravel.com/' => Http::response(),7]);89Http::get('https://laravel.com');10Http::get('https://nova.laravel.com/');1112$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
事件。
RequestSending
與 ConnectionFailed
事件都有一個 $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 array5 */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 array5 */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];