Laravel предоставляет выразительный API поверх Guzzle HTTP-клиента для выполнения исходящих HTTP-запросов. HTTP-клиент включает удобные инструменты для тестирования, retry-логики и параллельных запросов.
Выполнение запросов
Основные методы
use Illuminate\Support\Facades\Http;
// GET request
$response = Http::get('https://api.example.com/users');
// GET with query parameters
$response = Http::get('https://api.example.com/users', [
'page' => 1,
'per_page' => 25,
'sort' => 'created_at',
]);
// POST request
$response = Http::post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => '[email protected]',
]);
// PUT request
$response = Http::put('https://api.example.com/users/1', [
'name' => 'John Updated',
]);
// PATCH request
$response = Http::patch('https://api.example.com/users/1', [
'status' => 'active',
]);
// DELETE request
$response = Http::delete('https://api.example.com/users/1');
Работа с ответом
$response = Http::get('https://api.example.com/users');
// Response body
$body = $response->body(); // Raw string
$json = $response->json(); // Decoded JSON as array
$json = $response->json('data.0'); // Dot notation access
$object = $response->object(); // Decoded JSON as object
$collection = $response->collect(); // As Collection
$collection = $response->collect('data'); // Nested as Collection
// Status checks
$response->status(); // 200
$response->ok(); // true (200)
$response->successful(); // true (200-299)
$response->redirect(); // true (300-399)
$response->clientError(); // true (400-499)
$response->serverError(); // true (500-599)
$response->failed(); // true (client or server error)
// Specific status checks
$response->created(); // 201
$response->accepted(); // 202
$response->noContent(); // 204
$response->notFound(); // 404
$response->unauthorized(); // 401
$response->forbidden(); // 403
$response->unprocessable(); // 422
$response->tooManyRequests(); // 429
// Headers
$response->header('Content-Type');
$response->headers(); // All headers
Обработка ошибок
// Throw exception on client/server error
$response = Http::get('https://api.example.com/users')->throw();
// Throw with custom handling
$response = Http::get('https://api.example.com/users')
->throw(function ($response, $e) {
Log::error('API call failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
});
// Throw if specific condition
$response->throwIf($response->json('error') !== null);
$response->throwUnless($response->json('success') === true);
// Throw if failed (4xx or 5xx)
$response->throwIfStatus(429);
$response->throwUnlessStatus(200);
Настройка запроса
Заголовки
$response = Http::withHeaders([
'X-Api-Key' => config('services.external.api_key'),
'Accept' => 'application/json',
'X-Request-Id' => (string) Str::uuid(),
])->get('https://api.example.com/data');
// Content type
$response = Http::contentType('application/xml')
->accept('application/json')
->post('https://api.example.com/data', $xmlContent);
Аутентификация
// Bearer token
$response = Http::withToken($apiToken)->get('https://api.example.com/me');
// Basic auth
$response = Http::withBasicAuth('username', 'password')
->get('https://api.example.com/data');
// Digest auth
$response = Http::withDigestAuth('username', 'password')
->get('https://api.example.com/data');
Timeout
// Connection timeout + request timeout
$response = Http::timeout(30) // Request timeout in seconds
->connectTimeout(10) // Connection timeout in seconds
->get('https://api.example.com/data');
Отправка форм и файлов
// Form URL encoded
$response = Http::asForm()->post('https://api.example.com/login', [
'email' => '[email protected]',
'password' => 'secret',
]);
// Multipart / file upload
$response = Http::attach(
'avatar',
file_get_contents('/path/to/photo.jpg'),
'avatar.jpg'
)->post('https://api.example.com/profile', [
'name' => 'John Doe',
]);
// Multiple files
$response = Http::attach('document', $fileContents, 'report.pdf')
->attach('photo', $photoContents, 'me.jpg')
->post('https://api.example.com/upload');
Отправка сырого тела
// Raw body
$response = Http::withBody($xmlString, 'application/xml')
->post('https://api.example.com/xml-endpoint');
Повторные попытки (Retries)
// Retry 3 times with 100ms delay
$response = Http::retry(3, 100)->get('https://api.example.com/data');
// Retry with exponential backoff
$response = Http::retry(3, function (int $attempt, \Exception $exception) {
return $attempt * 200; // 200ms, 400ms, 600ms
})->get('https://api.example.com/data');
// Retry only on specific conditions
$response = Http::retry(3, 100, function (\Exception $exception, $response) {
// Retry only on server errors or rate limiting
return $exception instanceof ConnectionException
|| ($response && $response->status() === 429);
})->get('https://api.example.com/data');
// Retry with throw on final failure
$response = Http::retry(3, 100, throw: true)
->get('https://api.example.com/data');
::alert{type="info"}
retry(): Первый аргумент - количество попыток (3 = 1 основной + 2 повторных). Второй - задержка в миллисекундах (или callback). Третий - условие для повтора. throw: true выбрасывает исключение после исчерпания попыток.
::
Параллельные запросы (Concurrent Requests)
use Illuminate\Http\Client\Pool;
// Execute multiple requests concurrently
$responses = Http::pool(fn (Pool $pool) => [
$pool->get('https://api.example.com/users'),
$pool->get('https://api.example.com/orders'),
$pool->get('https://api.example.com/products'),
]);
// Access responses by index
$users = $responses[0]->json();
$orders = $responses[1]->json();
$products = $responses[2]->json();
// Named requests
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('users')->get('https://api.example.com/users'),
$pool->as('orders')->get('https://api.example.com/orders'),
$pool->as('products')->get('https://api.example.com/products'),
]);
$users = $responses['users']->json();
$orders = $responses['orders']->json();
$products = $responses['products']->json();
// With auth and options per request
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('github')
->withToken(config('services.github.token'))
->get('https://api.github.com/user'),
$pool->as('stripe')
->withToken(config('services.stripe.secret'))
->get('https://api.stripe.com/v1/customers'),
]);
Макросы
Макросы позволяют определить предустановленные конфигурации HTTP-клиента:
// In AppServiceProvider::boot()
use Illuminate\Support\Facades\Http;
Http::macro('github', function () {
return Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json',
])->withToken(config('services.github.token'))
->baseUrl('https://api.github.com');
});
Http::macro('stripe', function () {
return Http::withToken(config('services.stripe.secret'))
->baseUrl('https://api.stripe.com/v1')
->timeout(30)
->retry(3, 100);
});
Http::macro('internal', function () {
return Http::baseUrl(config('services.internal.base_url'))
->withHeaders([
'X-Service-Key' => config('services.internal.key'),
])
->timeout(10);
});
Использование:
// Clean, reusable API calls
$repos = Http::github()->get('/user/repos')->json();
$customers = Http::stripe()->get('/customers')->json();
$data = Http::internal()->get('/api/data')->json();
Middleware (Request Events)
use Illuminate\Http\Client\Events\RequestSending;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Http\Client\Events\ConnectionFailed;
// Listen to HTTP client events
Event::listen(RequestSending::class, function (RequestSending $event) {
Log::debug('HTTP Request', [
'method' => $event->request->method(),
'url' => $event->request->url(),
]);
});
Event::listen(ResponseReceived::class, function (ResponseReceived $event) {
Log::debug('HTTP Response', [
'url' => $event->request->url(),
'status' => $event->response->status(),
'duration' => $event->response->transferStats?->getTransferTime(),
]);
});
Event::listen(ConnectionFailed::class, function (ConnectionFailed $event) {
Log::error('HTTP Connection Failed', [
'url' => $event->request->url(),
]);
});
Практический пример: API-клиент
declare(strict_types=1);
namespace App\Services;
use App\DTOs\WeatherData;
use App\Exceptions\WeatherApiException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
final class WeatherService
{
private function client(): PendingRequest
{
return Http::baseUrl(config('services.weather.base_url'))
->withHeaders([
'X-Api-Key' => config('services.weather.api_key'),
])
->timeout(15)
->retry(3, 200, fn ($exception) => !$exception instanceof RequestException || $exception->response->serverError())
->throw();
}
public function getCurrentWeather(string $city): WeatherData
{
return Cache::remember(
"weather:{$city}",
now()->addMinutes(30),
function () use ($city) {
try {
$response = $this->client()->get('/current', [
'city' => $city,
'units' => 'metric',
]);
return WeatherData::fromArray($response->json('data'));
} catch (RequestException $e) {
throw new WeatherApiException(
"Failed to fetch weather for {$city}: " . $e->getMessage(),
previous: $e,
);
}
}
);
}
/**
* Fetch weather for multiple cities concurrently.
*
* @param array<int, string> $cities
* @return array<string, WeatherData>
*/
public function getWeatherForCities(array $cities): array
{
$responses = Http::pool(fn ($pool) => collect($cities)->map(
fn (string $city) => $pool
->as($city)
->withHeaders(['X-Api-Key' => config('services.weather.api_key')])
->baseUrl(config('services.weather.base_url'))
->timeout(15)
->get('/current', ['city' => $city, 'units' => 'metric'])
)->toArray());
$results = [];
foreach ($cities as $city) {
if ($responses[$city]->successful()) {
$results[$city] = WeatherData::fromArray($responses[$city]->json('data'));
}
}
return $results;
}
}
Тестирование HTTP-клиента
Фейковые ответы
use Illuminate\Support\Facades\Http;
// Fake ALL requests
Http::fake();
// Fake with specific response
Http::fake([
'api.example.com/users' => Http::response(['data' => []], 200),
'api.example.com/users/*' => Http::response(['name' => 'John'], 200),
'api.stripe.com/*' => Http::response(['error' => 'Unauthorized'], 401),
'*' => Http::response('Not Found', 404),
]);
// Fake with sequence
Http::fake([
'api.example.com/*' => Http::sequence()
->push(['data' => 'first'], 200)
->push(['data' => 'second'], 200)
->pushStatus(429)
->whenEmpty(Http::response(['data' => 'default'], 200)),
]);
// Fake with callback
Http::fake(function ($request) {
if (str_contains($request->url(), '/users')) {
return Http::response(['users' => []], 200);
}
return Http::response('', 404);
});
Assertions
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class WeatherServiceTest extends TestCase
{
public function test_fetches_current_weather(): void
{
Http::fake([
'api.weather.com/current*' => Http::response([
'data' => [
'temp' => 22.5,
'humidity' => 65,
'description' => 'Partly cloudy',
],
], 200),
]);
$service = app(WeatherService::class);
$weather = $service->getCurrentWeather('Moscow');
$this->assertEquals(22.5, $weather->temperature);
$this->assertEquals(65, $weather->humidity);
// Assert request was sent
Http::assertSent(function ($request) {
return str_contains($request->url(), '/current')
&& $request['city'] === 'Moscow'
&& $request['units'] === 'metric';
});
Http::assertSentCount(1);
}
public function test_handles_api_error(): void
{
Http::fake([
'api.weather.com/*' => Http::response('Server Error', 500),
]);
$this->expectException(WeatherApiException::class);
$service = app(WeatherService::class);
$service->getCurrentWeather('InvalidCity');
}
public function test_concurrent_requests(): void
{
Http::fake([
'api.weather.com/current*' => Http::response([
'data' => ['temp' => 20, 'humidity' => 50, 'description' => 'Clear'],
]),
]);
$service = app(WeatherService::class);
$results = $service->getWeatherForCities(['Moscow', 'London', 'Tokyo']);
$this->assertCount(3, $results);
Http::assertSentCount(3);
}
public function test_no_external_requests_made(): void
{
Http::fake();
// Action that should not make external calls
$this->get('/cached-page');
Http::assertNothingSent();
}
public function test_request_headers(): void
{
Http::fake();
Http::withToken('test-token')->get('https://api.example.com/data');
Http::assertSent(function ($request) {
return $request->hasHeader('Authorization', 'Bearer test-token');
});
}
}
Prevent Stray Requests
// Prevent any real HTTP requests during tests
Http::preventStrayRequests();
// Now any unfaked request will throw an exception
Http::get('https://real-api.com/data');
// Throws: Attempted to make a real HTTP request...