Почти каждому современному приложению необходимо взаимодействовать с чем-то внешним: платежным шлюзом, поставщиком услуг доставки, службой 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, разумные настройки по умолчанию и первоклассные помощники по тестированию.
Ключевые вещи, которые он дает вам из коробки:
- Свободное построение запроса : заголовки, аутентификация, параметры запроса и тело объединены в одно выражение.
- Автоматическая обработка JSON : массивы кодируются при отправке, а ответы декодируются при чтении.
- Объект ответа : богатая оболочка со вспомогательными функциями состояния, а не просто необработанная строка.
- Повторные попытки и таймауты : встроенные методы, никаких ручных циклов.
- Тестируемость : подделать любую конечную точку, не затрагивая сеть.
Реальный вариант использования
Теория стоит дешево, поэтому давайте привяжем все к одному сценарию. Представьте, что вы интегрируете стороннюю службу выставления счетов. Вам нужно:
- Создайте счет для клиента.
- Получите текущий статус счета-фактуры.
- Аутентифицируйте каждый запрос с помощью секретного ключа 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)
);Преимущества суммируются с остальной частью руководства:
- Безопасность типов : PHP обеспечивает форму полезных данных на границе, а не API.
- Автозаполнение : ваша IDE знает каждое поле, которое принимает запрос.
- Единый источник достоверной информации : форма полезной нагрузки определяется один раз, а не распространяется по местам вызова.
- Безопасные значения по умолчанию : необязательные поля (например,
валюта) получают разумные значения по умолчанию в одном месте. - Удобство рефакторинга : переименуйте поле, и система типов укажет вам при каждом его использовании.
Для 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() .
Следуйте этой структуре, и ваши интеграции будут легко изменять, легко тестировать и безопасно работать с реальными деньгами и реальными апстримами.
Комментарии (0)
Пока нет комментариев — будьте первым.