HTTP Client


简介

Laravel 基于 Guzzle HTTP 客户端封装了一个优雅的、最小化的 API,从而方便开发者快速创建 HTTP 请求与其他 Web 应用进行通信。Laravel 对 Guzzle 的封装围绕的是最常用的场景,并且致力于提供更好的开发体验。

开始使用之前,需要确保已经在项目中安装过 Guzzle 扩展包依赖,默认情况下,Laravel 会自动包含这个依赖:

composer require guzzlehttp/guzzle

创建请求

基本使用

要创建请求,可以使用 getpostputpatch 以及 delete 方法。首先,让我们看看如何创建最基本的 GET 请求:

use Illuminate\Support\Facades\Http;

$response = Http::get('http://example.com');

get 方法返回一个 Illuminate\Http\Client\Response 实例,该实例上提供了多个对响应进行「透视」的方法:

$response->body() : string;
$response->json($key = null, $default = null) : array|mixed;
$response->object() : object;
$response->collect($key = null) : Illuminate\Support\Collection;
$response->status() : int;
$response->successful() : bool;
$response->redirect(): bool;
$response->failed() : bool;
$response->clientError() : bool;
$response->header($header) : string;
$response->headers() : array;

Illuminate\Http\Client\Response 对象还实现了 PHP ArrayAccess 接口,这样一来,就可以直接在响应实例上访问 JSON 响应数据:

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

除了上面列出的响应方法之外,还可以使用以下方法来确定响应是否具有给定的状态码:

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

URI 模板

HTTP 客户端还允许您使用 URI 模板规范构建请求 URL。为了定义可以由 UR I模板扩展的 URL 参数,您可以使用 withUrlParameters 方法:

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

请求转储

如果您想在发送请求之前转储出站请求实例并终止脚本的执行,可以在请求定义的开头添加 dd 方法:

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

请求数据

当然,在 POSTGETPATCH 请求中发送额外请求数据很常见,这些方法可以接收数组格式请求数据作为第二个参数。默认情况下,数据会以 application/json 内容类型发送:

$response = Http::post('http://example.com/users', [
    'name' => 'Steve',
    'role' => 'Network Administrator',
]);

GET 请求查询字符串

发送 GET 请求时,查询字符串可以直接添加到 URL 或者以键值对数组形式作为 get 方法的第二个参数传递:

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

可以使用 withQueryParameters 方法进行替代:

Http::retry(3, 100)->withQueryParameters([
    'name' => 'Taylor',
    'page' => 1,
])->get('http://example.com/users')

发送表单 URL 编码请求

如果你想使用 application/x-www-form-urlencoded内容类型发送数据(一般 HTML 表单都是这个格式),需要在创建请求前调用 asForm 方法:

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

发送原生请求实体

你可以在发起请求时,使用 withBody 方法提供原生的请求实体:

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

Multi-Part 请求

对于文件上传请求,需要以 multi-part 内容类型发起请求,这可以通过在创建请求前调用 attach 方法完成。该方法接收文件名和内容作为参数,还可以传递第三个参数作为期望文件名:

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

除了传递文件原生内容外,还可以传递流资源:

$photo = fopen('photo.jpg', 'r');

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

请求头

可以使用 withHeaders 方法添加请求头到请求,withHeaders 方法接收数据格式是键值对数组:

$response = Http::withHeaders([
    'X-First' => 'foo',
    'X-Second' => 'bar'
])->post('http://example.com/users', [
    'name' => '学院君',
]);

你可以使用 accept 方法来指定应用程序对请求的响应中期望的内容类型:

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

为了方便起见,你可以使用 acceptJson 方法来快速指定应用程序期望的 application/json 内容类型作为响应:

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

withHeaders 方法将新的头信息合并到请求的现有头信息中。如果需要,你可以使用 replaceHeaders 方法完全替换所有头信息:

$response = Http::withHeaders([
    'X-Original' => 'foo',
])->replaceHeaders([
    'X-Replacement' => 'bar',
])->post('http://example.com/users', [
    'name' => '学院君',
]);

认证

你可以使用 withBasicAuthwithDigestAuth 方法设置认证方式:

// HTTP Basic 认证...
$response = Http::withBasicAuth('taylor@laravel.com', 'secret')->post(...);

// HTTP Digest 认证...
$response = Http::withDigestAuth('taylor@laravel.com', 'secret')->post(...);

Bearer Tokens

如果你想要快速添加 Authorization Bearer Token 头到请求,可以使用 withToken 方法:

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

超时

可以通过 timeout 方法指定等待响应的最大时长(单位:秒),即请求的超时时间:

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

如果给定的超时时间已到,则会抛出 Illuminate\Http\Client\ConnectionException 异常。

您可以使用 connectTimeout 方法来指定连接服务器时的最大等待时间(以秒为单位):

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

重试

如果你想要 HTTP 客户端在客户端或服务端发生错误时自动重发请求,可以使用 retry 方法。该方法接收两个参数 —— 重试次数和两次重试之间的时间间隔(ms):

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

如有需要,您可以向 retry 方法传递第三个参数。第三个参数应该是一个确定是否实际进行重试的可调用对象。例如,如果初始请求遇到 ConnectionException,则可能只希望重试该请求:

use Exception;
use Illuminate\Http\Client\PendingRequest;
 
$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {
    return $exception instanceof ConnectionException;
})->post(/* ... */);

如果请求尝试失败,您可能希望在进行新尝试之前对请求进行修改。您可以通过修改提供给 retry 方法的可调用对象的请求参数来实现这一目的。例如,如果第一次尝试返回身份验证错误,您可能希望使用新的授权令牌重试该请求:

use Exception;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
 
$response = Http::withToken($this->getToken())->retry(2, 0, function (Exception $exception, PendingRequest $request) {
    if (! $exception instanceof RequestException || $exception->response->status() !== 401) {
        return false;
    }
 
    $request->withToken($this->getNewToken());
 
    return true;
})->post(/* ... */);

如果所有请求都失败,将抛出一个 Illuminate\Http\Client\RequestException 实例。如果您想禁用此行为,可以提供一个 throw 参数,其值为 false。禁用后,客户端在尝试了所有重试后,将返回最后收到的响应:

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

即使 throw 参数设置为 false,如果所有请求因连接问题失败,则仍会抛出Illuminate\Http\Client\ConnectionException

错误处理

不同于 Guzzle 的默认行为,Laravel 的 HTTP 客户端封装在发生客户端或服务器错误(从服务器返回的 400500 级别的响应)时不会抛出异常。您可以使用 successfulclientErrorserverError 方法判断是否返回了这些错误之一:

// 判断状态码是否大于等于200且小于300...
$response->successful();

// 判断状态码是否大于等于400...
$response->failed();

// 判断响应是否具有400级别的状态码...
$response->clientError();

// 判断响应是否具有500级别的状态码...
$response->serverError();

// 如果存在客户端或服务器错误,立即执行给定的回调函数...
$response->onError(callable $callback);

抛出异常

如果你有一个响应实例,并且希望在响应状态码表示客户端或服务器错误时,抛出一个 Illuminate\Http\Client\RequestException 的实例,可以使用 throwthrowIf 方法:

use Illuminate\Http\Client\Response;

$response = Http::post(/* ... */);

// 如果发生客户端或服务器错误,则抛出异常...
$response->throw();

// 如果发生错误,并且给定的条件为真,则抛出异常...
$response->throwIf($condition);

// 如果发生错误,并且给定的闭包返回 true,则抛出异常...
$response->throwIf(fn (Response $response) => true);

// 如果发生错误,并且给定的条件为假,则抛出异常...
$response->throwUnless($condition);

// 如果发生错误,并且给定的闭包返回 false,则抛出异常...
$response->throwUnless(fn (Response $response) => false);

// 如果响应具有特定的状态码,则抛出异常...
$response->throwIfStatus(403);

// 如果响应不具有特定的状态码,则抛出异常...
$response->throwUnlessStatus(200);

return $response['user']['id'];

Illuminate\Http\Client\RequestException 实例具有一个公共的 $response 属性,允许您检查返回的响应。

如果没有发生错误,throw 方法将返回响应实例,允许您在 throw 方法上链接其他操作:

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

如果在抛出异常之前想要执行其他逻辑,可以将闭包传递给 throw 方法。在调用闭包后,异常将自动抛出,因此您无需在闭包内部重新抛出异常:

use Illuminate\Http\Client\Response;
use Illuminate\Http\Client\RequestException;
 
return Http::post(/* ... */)->throw(function (Response $response, RequestException $e) {
    // ...
})->json();

Guzzle 中间件

由于 Laravel 的 HTTP 客户端使用 Guzzle,您可以利用 Guzzle 中间件来操作传出请求或检查传入响应。要操作传出请求,请通过 withRequestMiddleware 方法注册一个 Guzzle 中间件:

use Illuminate\Support\Facades\Http;
use Psr\Http\Message\RequestInterface;
 
$response = Http::withRequestMiddleware(
    function (RequestInterface $request) {
        return $request->withHeader('X-Example', 'Value');
    }
)->get('http://example.com');

同样,您可以通过 withResponseMiddleware 方法注册一个中间件来检查传入的 HTTP 响应:

use Illuminate\Support\Facades\Http;
use Psr\Http\Message\ResponseInterface;
 
$response = Http::withResponseMiddleware(
    function (ResponseInterface $response) {
        $header = $response->getHeader('X-Example');
 
        // ...
 
        return $response;
    }
)->get('http://example.com');

全局中间件

有时,您可能希望注册一个适用于每个传出请求和传入响应的中间件。为此,您可以使用 globalRequestMiddlewareglobalResponseMiddleware 方法。通常,这些方法应在应用程序的 AppServiceProviderboot 方法中调用:

use Illuminate\Support\Facades\Http;
 
Http::globalRequestMiddleware(fn ($request) => $request->withHeader(
    'User-Agent', 'Example Application/1.0'
));
 
Http::globalResponseMiddleware(fn ($response) => $response->withHeader(
    'X-Finished-At', now()->toDateTimeString()
));

Guzzle 选项

你可以使用 withOptions 方法指定更多额外的 Guzzle 请求选项,该方法接收数组格式参数来指定多个选项:

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

并发请求

有时候,您可能希望同时发起多个 HTTP 请求。换句话说,您希望多个请求同时发送,而不是依次发起请求。这可以在与慢速 HTTP API 交互时显著提高性能。

幸运的是,您可以使用 pool方法实现这一点。pool 方法接受一个闭包,该闭包接收一个 Illuminate\Http\Client\Pool 实例,让您可以轻松地将请求添加到请求池中以进行调度:

use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
 
$responses = Http::pool(fn (Pool $pool) => [
    $pool->get('http://localhost/first'),
    $pool->get('http://localhost/second'),
    $pool->get('http://localhost/third'),
]);
 
return $responses[0]->ok() &&
       $responses[1]->ok() &&
       $responses[2]->ok();

如您所见,每个响应实例可以根据其添加到池中的顺序进行访问。如果希望,您可以使用 as 方法为请求命名,这样您可以通过名称访问相应的响应:

use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
 
$responses = Http::pool(fn (Pool $pool) => [
    $pool->as('first')->get('http://localhost/first'),
    $pool->as('second')->get('http://localhost/second'),
    $pool->as('third')->get('http://localhost/third'),
]);
 
return $responses['first']->ok();

自定义并发请求

pool 方法无法与其他 HTTP 客户端方法(如 withHeadersmiddleware 方法)链接。如果要对汇集的请求应用自定义标头或中间件,应在每个请求中配置这些选项:

use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
 
$headers = [
    'X-Example' => 'example',
];
 
$responses = Http::pool(fn (Pool $pool) => [
    $pool->withHeaders($headers)->get('http://laravel.test/test'),
    $pool->withHeaders($headers)->get('http://laravel.test/test'),
    $pool->withHeaders($headers)->get('http://laravel.test/test'),
]);

Laravel HTTP 客户端允许您定义“宏”,它可以作为一种流畅、表达性的机制,用于在整个应用程序中与服务交互时配置常见的请求路径和标头。要开始使用宏,可以在应用程序的 App\Providers\AppServiceProvider 类的 boot方法中定义宏:

use Illuminate\Support\Facades\Http;
 
/**
 * 初始化任何应用程序服务
 */
public function boot(): void
{
    Http::macro('github', function () {
        return Http::withHeaders([
            'X-Example' => 'example',
        ])->baseUrl('https://github.com');
    });
}

配置好宏后,您可以在应用程序的任何地方调用它以创建带有指定配置的待处理请求:

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

测试

很多 Laravel 服务都提供了助力开发者轻松优雅编写测试代码的功能,Laravel HTTP 客户端封装库也不例外,通过 HTTP 门面的 fake 方法,你可以轻松构造 HTTP 客户端并在发起请求后返回预定义(通过桩文件或者模板代码设置)响应。

伪造响应

例如,要构建一个 HTTP 客户端针对任意请求都返回空实体、200 状态码响应,可以调用不传入任何参数的 fake 方法实现:

use Illuminate\Support\Facades\Http;

Http::fake();

$response = Http::post(...);

伪造指定 URL

你还可以传递数组到 fake 方法,该数组的键表示你希望伪造的请求 URL 模式字符串,该数组的值表示与之关联的响应。在 URL 模式字符串中可以使用 * 作为通配符。在预定义响应中,我们使用的是 response 方法来构建响应:

Http::fake([
    // Stub a JSON response for GitHub endpoints...
    'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']),

    // Stub a string response for Google endpoints...
    'google.com/*' => Http::response('Hello World', 200, ['Headers']),
]);

如果你想要指定兜底 URL 模式来处理所有未匹配请求,可以使用 *作为键:

Http::fake([
    // Stub a JSON response for GitHub endpoints...
    'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']),

    // Stub a string response for all other endpoints...
    '*' => Http::response('Hello World', 200, ['Headers']),
]);

伪造响应序列

有时候,你可能需要指定单个 URL 以特定顺序返回多个伪造响应,这可以通过使用 Http::sequence 方法构建响应来实现:

Http::fake([
    // Stub a series of responses for GitHub endpoints...
    'github.com/*' => Http::sequence()
                            ->push('Hello World', 200)
                            ->push(['foo' => 'bar'], 200)
                            ->pushStatus(404),
]);

当响应序列中的所有响应都已经被消费,新请求进来会抛出异常,如果你想要在序列为空的情况下指定默认响应,可以使用 whenEmpty 方法:

Http::fake([
    // Stub a series of responses for GitHub endpoints...
    'github.com/*' => Http::sequence()
                            ->push('Hello World', 200)
                            ->push(['foo' => 'bar'], 200)
                            ->whenEmpty(Http::response()),
]);

如果你想要伪造响应序列但不需要指定特定的 URL 模式,可以使用 Http::fakeSequence 方法:

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

伪造回调

如果你需要更复杂的逻辑来判断特定端点返回什么响应,可以传递一个回调函数到 fake 方法。该回调函数接收一个 Illuminate\Http\Client\Request 实例并返回一个对应的响应实例:

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

防止偏离请求

如果您希望确保通过 HTTP 客户端发送的所有请求在个别测试或完整测试套件中都被伪造,您可以调用 preventStrayRequests 方法。调用此方法后,任何没有对应伪造响应的请求将会抛出异常,而不是进行实际的 HTTP 请求:

use Illuminate\Support\Facades\Http;
 
Http::preventStrayRequests();
 
Http::fake([
    'github.com/*' => Http::response('ok'),
]);
 
// An "ok" response is returned...
Http::get('https://github.com/laravel/framework');
 
// An exception is thrown...
Http::get('https://laravel.com');

检查请求

伪造响应时,你可能想要检查客户端请求以便确保应用发送的是正确的数据或者请求头。这可以通过在调用 Http::fake 之后调用 Http::assertSent 方法来实现。

assertSent 方法接收一个回调函数作为参数,该回调函数需要传入 Illuminate\Http\Client\Request 实例然后返回一个布尔类型的值,用来表明请求是否和预期匹配。为了让测试通过,至少要发出一个和预期匹配的请求:

use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

Http::fake();

Http::withHeaders([
    'X-First' => 'foo',
])->post('http://example.com/users', [
    'name' => '学院君',
    'role' => 'Developer',
]);

Http::assertSent(function (Request $request) {
    return $request->hasHeader('X-First', 'foo') &&
           $request->url() == 'http://example.com/users' &&
           $request['name'] == '学院君' &&
           $request['role'] == 'Developer';
});

如果需要的话,你可以使用 assertNotSent 方法断言指定请求没有发送:

use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

Http::fake();

Http::post('http://blog.test/users', [
    'name' => '学院君',
    'role' => 'Developer',
]);

Http::assertNotSent(function (Request $request) {
    return $request->url() === 'http://example.com/posts';
});

您可以使用 assertSentCount 方法来断言测试过程中发送的请求数量:

Http::fake();

Http::assertSentCount(5);

或者,您可以使用 assertNothingSent 方法来断言测试过程中没有发送任何请求:

Http::fake();
 
Http::assertNothingSent();

记录请求/响应

您可以使用 recorded 方法来收集所有的请求和对应的响应。recorded 方法返回一个包含Illuminate\Http\Client\RequestIlluminate\Http\Client\Response 实例的数组集合:

Http::fake([
    'https://laravel.com' => Http::response(status: 500),
    'https://nova.laravel.com/' => Http::response(),
]);
 
Http::get('https://laravel.com');
Http::get('https://nova.laravel.com/');
 
$recorded = Http::recorded();
 
[$request, $response] = $recorded[0];

此外,recorded 方法接受一个闭包,闭包将接收 Illuminate\Http\Client\RequestIlluminate\Http\Client\Response 实例,并可用于根据您的期望筛选请求/响应对:

use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\Response;
 
Http::fake([
    'https://laravel.com' => Http::response(status: 500),
    'https://nova.laravel.com/' => Http::response(),
]);
 
Http::get('https://laravel.com');
Http::get('https://nova.laravel.com/');
 
$recorded = Http::recorded(function (Request $request, Response $response) {
    return $request->url() !== 'https://laravel.com' &&
           $response->successful();
});

事件

Laravel 在发送 HTTP 请求的过程中触发了三个事件。在发送请求之前,会触发 RequestSending 事件;在收到给定请求的响应后,会触发 ResponseReceived 事件;如果给定请求未收到响应,则触发 ConnectionFailed 事件。

RequestSendingConnectionFailed 事件都包含一个名为 $request 的公共属性,您可以用它来检查Illuminate\Http\Client\Request 实例。同样,ResponseReceived 事件包含 $request$response 属性,您可以用它们来检查 Illuminate\Http\Client\Response 实例。您可以在 App\Providers\EventServiceProvider 服务提供者中注册此事件的事件监听器:

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'Illuminate\Http\Client\Events\RequestSending' => [
        'App\Listeners\LogRequestSending',
    ],
    'Illuminate\Http\Client\Events\ResponseReceived' => [
        'App\Listeners\LogResponseReceived',
    ],
    'Illuminate\Http\Client\Events\ConnectionFailed' => [
        'App\Listeners\LogConnectionFailed',
    ],
];

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 辅助函数

>> 下一篇: 本地化