Почти каждому современному приложению необходимо взаимодействовать с чем-то внешним: платежным шлюзом, поставщиком услуг доставки, службой SMS, внутренним микросервисом. В течение многих лет разработчики PHP напрямую обращались к cURL или использовали Guzzle и подключали его вручную. HTTP-клиент Laravel — это гибкая оболочка Guzzle, которая удаляет этот шаблон и предоставляет вам чистый, выразительный и тестируемый API для выполнения исходящих запросов.

В этом руководстве описывается построение реальной интеграции так, как вы структурируете ее в рабочей среде: в виде класса обслуживания, управляемого конфигурацией, с правильной обработкой ответов, обработкой ошибок и журналированием.

Что такое HTTP-клиент?

HTTP-клиент — это встроенный интерфейс Laravel для выполнения исходящих HTTP-запросов. Он поставляется вместе с фреймворком (под капотом которого находится Guzzle), и доступ к нему осуществляется через фасад Http  .

use Illuminate\Support\Facades\Http;

$response = Http::get('https://api.example.com/users');

Эта единственная строка обрабатывает создание клиента, отправку запроса и перенос результата в объект ответа, который вы можете запросить. По сравнению с необработанным Guzzle вы получаете более читаемый синтаксис, автоматическое кодирование/декодирование JSON, разумные настройки по умолчанию и первоклассные помощники по тестированию.

Ключевые вещи, которые он дает вам из коробки:

  1. Свободное построение запроса : заголовки, аутентификация, параметры запроса и тело объединены в одно выражение.
  2. Автоматическая обработка JSON : массивы кодируются при отправке, а ответы декодируются при чтении.
  3. Объект ответа : богатая оболочка со вспомогательными функциями состояния, а не просто необработанная строка.
  4. Повторные попытки и таймауты : встроенные методы, никаких ручных циклов.
  5. Тестируемость : подделать любую конечную точку, не затрагивая сеть.

Реальный вариант использования

Теория стоит дешево, поэтому давайте привяжем все к одному сценарию. Представьте, что вы интегрируете стороннюю службу выставления счетов. Вам нужно:

  • Создайте счет для клиента.
  • Получите текущий статус счета-фактуры.
  • Аутентифицируйте каждый запрос с помощью секретного ключа API.
  • Укажите URL-адрес песочницы в разработке и URL-адрес рабочей среды в реальном времени.

Наивная версия выглядит так:

$response = Http::withToken('sk_live_xxxxx')
->post('https://api.invoicer.com/v1/invoices', [
'customer_id' => 1,
'amount' => 150000,
'currency' => 'IDR',
]);

$invoice = $response->json();

Это работает, но секрет жестко запрограммирован, базовый URL повторяется повсюду и обработка ошибок не осуществляется. Оставшаяся часть этого руководства реорганизует его во что-то, что вы действительно сможете реализовать.

Создание класса обслуживания

Рассредоточение вызовов Http::  по контроллерам затрудняет изменение интеграции и делает невозможным чистое тестирование. Стандартный шаблон заключается в том, чтобы обернуть каждую внешнюю службу в отдельный класс под номером app/Services .

<?php

namespace App\Services;

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

class InvoicerService
{
protected function client(): PendingRequest
{
return Http::baseUrl(config('services.invoicer.base_url'))
->withToken(config('services.invoicer.secret'))
->acceptJson()
->timeout(15);
}

public function createInvoice(array $payload): array
{
return $this->client()
->post('/v1/invoices', $payload)
->json();
}

public function getInvoice(string $id): array
{
return $this->client()
->get("/v1/invoices/{$id}")
->json();
}
}

Теперь контроллеры зависят от InvoicerService  , а не от деталей HTTP. Метод client()  централизует базовый URL-адрес, аутентификацию и настройки по умолчанию, поэтому каждый вызов использует одну и ту же конфигурацию.

Совет: ввод полезных данных с помощью DTO

Передача необработанных массивов в ваши сервисные методы работает, но ненадежно. Опечатка в ключе ( custmer_id  ), отсутствие обязательного поля или неправильный тип не будут обнаружены, пока API не отклонит запрос во время выполнения. Объект передачи данных (DTO) исправляет это, превращая полезные данные в типизированную самопроверяющуюся структуру, которую IDE и PHP могут проверять за вас.

Определите DTO только для чтения для запроса:

<?php

namespace App\DataTransferObjects;

class CreateInvoiceData
{
public function __construct(
public readonly int $customerId,
public readonly int $amount,
public readonly string $currency = 'IDR',
) {}

public static function fromArray(array $data): self
{
return new self(
customerId: $data['customer_id'],
amount: $data['amount'],
currency: $data['currency'] ?? 'IDR',
);
}

public function toArray(): array
{
return [
'customer_id' => $this->customerId,
'amount' => $this->amount,
'currency' => $this->currency,
];
}
}

Затем введите метод службы для DTO вместо свободного массива :

public function createInvoice(CreateInvoiceData $data): array
{
return $this->client()
->post('/v1/invoices', $data->toArray())
->throw()
->json();
}

Теперь место вызова является явным, и его невозможно ошибиться:

$invoice = $this->invoicer->createInvoice(
new CreateInvoiceData(customerId: 1, amount: 150000)
);

Преимущества суммируются с остальной частью руководства:

  1. Безопасность типов : PHP обеспечивает форму полезных данных на границе, а не API.
  2. Автозаполнение : ваша IDE знает каждое поле, которое принимает запрос.
  3. Единый источник достоверной информации : форма полезной нагрузки определяется один раз, а не распространяется по местам вызова.
  4. Безопасные значения по умолчанию : необязательные поля (например, валюта  ) получают разумные значения по умолчанию в одном месте.
  5. Удобство рефакторинга : переименуйте поле, и система типов укажет вам при каждом его использовании.

Для DTO с более тяжелыми потребностями в сопоставлении или проверке spatie/laravel-data   — популярный пакет, который добавляет приведение, проверку и гидратацию массива/запроса поверх этого шаблона, но простые классы только для чтения, подобные приведенному выше, охватывают большинство интеграций с нулевыми зависимостями.

Хранение секретов и значений конфигурации

Обратите внимание, что служба читает из config('services.invoicer.*')  вместо того, чтобы что-либо жестко запрограммировать. Секреты и значения, зависящие от среды, относятся к конфигурации, полученной из вашего файла .env  .

Добавьте учетные данные в .env   :

INVOICER_BASE_URL=https://sandbox.api.invoicer.com
INVOICER_SECRET=sk_test_xxxxxxxxxxxxx

Затем зарегистрируйте их в config/services.php   , который является обычным местом хранения сторонних учетных данных в Laravel:

return [
// ... other services
'invoicer' => [
'base_url' => env('INVOICER_BASE_URL', 'https://sandbox.api.invoicer.com'),
'secret' => env('INVOICER_SECRET'),
],
];

Это защищает секреты от контроля версий, позволяет каждой среде предоставлять свои собственные значения и означает, что переключение из песочницы в рабочую среду осуществляется одним изменением .env  . Всегда читайте config() , а не env()  непосредственно в коде, чтобы кэширование конфигурации ( php artisan config:cache ) продолжало работать.

Выполнение REST-запросов

Имея службу и конфигурацию, вы можете выполнять полный спектр вызовов REST. HTTP-клиент четко сопоставляется с HTTP-глаголами.

// GET with query parameters
$this->client()->get('/v1/invoices', ['status' => 'pending']);

// POST with a JSON body (arrays are encoded automatically)
$this->client()->post('/v1/invoices', [
'customer_id' => 1,
'amount' => 150000,
'currency' => 'IDR',
]);

// PUT / PATCH to update
$this->client()->patch("/v1/invoices/{$id}", ['status' => 'paid']);

// DELETE
$this->client()->delete("/v1/invoices/{$id}");

Несколько распространенных модификаторов, о которых стоит знать:

// Send as form-encoded instead of JSON
Http::asForm()->post($url, $payload);

// Send multipart (file upload)
Http::attach('document', $fileContents, 'invoice.pdf')->post($url);

// Custom headers per request
Http::withHeaders(['X-Idempotency-Key' => $key])->post($url, $payload);

Обработка ответа

Каждый запрос возвращает объект Illuminate\Http\Client\Response , а не необработанную строку. Он предоставляет помощников для чтения тела и проверки результата.

$response = $this->client()->get("/v1/invoices/{$id}");
$response->json(); // decoded array
$response->json('status'); // dot-access a single key
$response->object(); // decoded as stdClass
$response->body(); // raw string body
$response->status(); // HTTP status code, e.g. 200
$response->headers(); // response headers

Помощники по статусу позволяют вам выразительно переходить к результату вместо сравнения чисел:

$response->successful();   // 200–299
$response->failed(); // 400 or higher
$response->clientError(); // 400–499
$response->serverError(); // 500–599
$response->notFound(); // 404

Обработка ошибок

По умолчанию HTTP-клиент не выдает ответы 4xx/5xx; он возвращает ответ, который вы должны проверить. Это частый источник скрытых ошибок, когда код предполагает успех и считывает json()  из тела ошибки.

У вас есть две стратегии. Первый — явная проверка:

$response = $this->client()->post('/v1/invoices', $payload);
if ($response->failed()) {
// handle the error case
}

return $response->json();

Второй, зачастую более простой способ — включить исключения с помощью throw()  , что вызывает RequestException  для любого 4xx или 5xx:

public function createInvoice(array $payload): array
{
return $this->client()
->post('/v1/invoices', $payload)
->throw()
->json();
}

Перехватите его там, где вы можете действовать, и вы получите ошибочный ответ, прикрепленный к исключению:

use Illuminate\Http\Client\RequestException;
try {
$invoice = $this->invoicer->createInvoice($payload);
} catch (RequestException $e) {
$status = $e->response->status();
$body = $e->response->json();
// map to a domain exception, retry, or surface to the user
}

Вы также можете выдать условный вызов, что полезно, когда ожидаются некоторые ответы, отличные от 2xx (скажем, 404 при поиске статуса):

$response->throwIf($response->serverError());
$response->throwUnless($response->successful());

Добавление таймаутов и повторов

Сетевые вызовы не выполняются. Надежная интеграция устанавливает тайм-аут, чтобы зависший восходящий поток не блокировал ваш запрос, и повторяет временные сбои с отсрочкой.

protected function client(): PendingRequest
{
return Http::baseUrl(config('services.invoicer.base_url'))
->withToken(config('services.invoicer.secret'))
->acceptJson()
->timeout(15) // max seconds for the whole request
->connectTimeout(5) // max seconds to establish connection
->retry(3, 200); // 3 attempts, 200ms base backoff
}

Для более точного контроля retry()  принимает замыкание, чтобы решить, стоит ли повторять данную ошибку (повторите попытку при ошибках 5xx и соединения, но не при ошибке проверки 422):

use Illuminate\Http\Client\ConnectionException;

->retry(3, 200, function ($exception, $request) {
return $exception instanceof ConnectionException
|| $exception->response?->serverError();
});

Регистрация запросов и ответов

Для любой интеграции, которая касается денег или внешнего состояния, вам потребуется запись того, что было отправлено и что вернулось. Самое чистое место для входа — внутри службы, вокруг вызова.

use Illuminate\Support\Facades\Log;

public function createInvoice(array $payload): array
{
$response = $this->client()->post('/v1/invoices', $payload);
Log::channel('invoicer')->info('createInvoice', [
'status' => $response->status(),
'payload' => $payload,
'response' => $response->json(),
]);
return $response->throw()->json();
}

Определите выделенный канал журнала в config/logging.php  , чтобы журналы внешних вызовов оставались отдельно от журналов вашего приложения:

'channels' => [
'invoicer' => [
'driver' => 'daily',
'path' => storage_path('logs/invoicer.log'),
'days' => 14,
],
],

Важное правило: никогда не регистрируйте необработанные секреты. Удалите или замаскируйте заголовок Authorization   и все конфиденциальные поля, прежде чем они попадут в журнал. Для обеспечения видимости в масштабе всего приложения вы также можете зарегистрировать глобальное промежуточное программное обеспечение, которое регистрирует каждый исходящий запрос, но ведение журнала для каждой службы сохраняет контекст более жестким.

Тестирование с помощью Http::fake()

Отличительной особенностью HTTP-клиента является простота тестирования. Http::fake()  перехватывает исходящие запросы, поэтому ваши тесты никогда не затрагивают сеть и возвращают любые определенные вами ответы.

use Illuminate\Support\Facades\Http;

Http::fake([
'api.invoicer.com/v1/invoices' => Http::response([
'id' => 'inv_123',
'status' => 'pending',
], 201),
]);

$invoice = app(InvoicerService::class)->createInvoice([
'customer_id' => 1,
'amount' => 150000,
'currency' => 'IDR',
]);
$this->assertEquals('inv_123', $invoice['id']);

Вы также можете убедиться, что запрос был выполнен правильно, проверив URL, метод, заголовки и полезную нагрузку:

Http::assertSent(function ($request) {
return $request->url() === 'https://api.invoicer.com/v1/invoices'
&& $request->method() === 'POST'
&& $request['amount'] === 150000;
});

Чтобы проверить пути сбоя, подделайте статус ошибки и убедитесь, что ваша обработка ошибок работает:

Http::fake([
'*' => Http::response(['message' => 'Unauthorized'], 401),
]);

$this->expectException(RequestException::class);

app(InvoicerService::class)->createInvoice($payload);

Это позволяет охватить как успешные, так и неудачные ветви каждой интеграции без использования живой песочницы.

Отправка запросов в параллельном режиме

Когда вам нужно обратиться к нескольким независимым конечным точкам, их последовательная отправка приведет к потере времени. Http::pool()  запускает их одновременно и возвращает все ответы одновременно.

use Illuminate\Http\Client\Pool;

$responses = Http::pool(fn (Pool $pool) => [
$pool->get('https://api.invoicer.com/v1/invoices/inv_1'),
$pool->get('https://api.invoicer.com/v1/invoices/inv_2'),
$pool->get('https://api.invoicer.com/v1/invoices/inv_3'),
]);

$first = $responses[0]->json();

Вы также можете присвоить имя каждому запросу в пуле и получить доступ к ответам по ключу, что позволит читать информацию, даже если вызовы различаются:

$responses = Http::pool(fn (Pool $pool) => [
$pool->as('customer')->get('/v1/customers/1'),
$pool->as('invoices')->get('/v1/invoices?customer_id=1'),
]);

$customer = $responses['customer']->json();
$invoices = $responses['invoices']->json();

Заключение

HTTP-клиент Laravel превращает исходящие запросы из шаблона cURL в чистый, выразительный код. Само по себе это удобно; в сочетании с несколькими моделями производства он становится надежным.

Эту дугу стоит усвоить: обернуть каждую внешнюю службу в отдельный класс, передавать учетные данные и URL-адреса через конфигурацию, а не жестко запрограммировать их, всегда явно обрабатывать ответы, отличные от 2xx (или выбирать throw()  ), добавлять таймауты и повторные попытки для устойчивости, регистрировать каждый внешний вызов с замаскированными секретами и покрывать как успех, так и неудачу с помощью Http::fake()  .

Следуйте этой структуре, и ваши интеграции будут легко изменять, легко тестировать и безопасно работать с реальными деньгами и реальными апстримами.