Laravel: Советы и рекомендации по работе с HTTP клиентом

Источник: «5 Tips and Tricks for working with the Laravel HTTP Client»
Как веб-разработчикам, нам часто приходится взаимодействовать с API из Laravel приложений. HTTP-клиент Laravel, представленный в седьмой версии — удобная и интуитивно понятная оболочка для Guzzle HTTP библиотеки. В этой статье мы рассмотрим пять полезных приёмов работы с HTTP-клиентом Laravel, которые помогут сделать ваше разработку более эффективной и приятной.

Эти приёмы включают использование HTTP-макросов, настройку HTTP-клиента для контейнерных служб, настройку базовой конфигурации URL, предотвращение случайных запросов в тестах и прослушивание HTTP-событий. Усвоив эти советы, вы сможете оптимизировать взаимодействие с API и создавать более надёжные и удобные в сопровождении приложения Laravel.

HTTP макрос

Многие сервисы Laravel имеют функцию макро позволяющую определять собственные методы для приложения. Вместо того чтобы расширять базовые классы из Laravel Framework, можно добавить эти макросы в метод boot() в сервис провайдере.

В HTTP документации показан пример макроса, который можно использовать для определения общих настроек:

public function boot(): void
{
Http::macro('github', function () {
return Http::withHeaders([
'X-Example' => 'example',
])->baseUrl('https://github.com');
});
}

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

Макросы могут определять любые удобные методы, которые вы хотели бы определить и повторно использовать в своём приложении. Пример документации макросов затрагивает ещё один совет по настройке HTTP-клиентов для использования в других сервисах.

Мы вернёмся к объединению макросов с передачей клиентов другим контейнерным сервисам в следующем разделе.

Настройка HTTP-клиента для Container Service

При взаимодействии с API из приложения Laravel вам, вероятно, понадобятся различные настраиваемые параметры для клиента. Например, если API имеет несколько сред, понадобится настраиваемый базовый URL-адрес, токен, параметры тайм-аута и многое другое.

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

Во-первых, давайте рассмотрим определение параметров клиента в методе register() сервис провайдера:

public function register(): void
{
$this->app->singleton(ExampleService::class, function (Application $app) {
$client = Http::withOptions([
'base_uri' => config('services.example.base_url'),
'timeout' => config('services.example.timeout', 10),
'connect_timeout' => config('services.example.connect_timeout', 2),
])->withToken(config('services.example.token'));

return new ExampleService($client);
});
}

В определении сервиса-одиночки мы связали несколько вызовов для настройки клиента. Результатом является экземпляр PendingRequest, который можно передать конструктору сервиса следующим образом:

class ExampleService
{
public function __construct(
private PendingRequest $client
) {}

public function getWidget(string $uid)
{
$response = $this->client
->withUrlParameters(['uid' => $uid])
->get('widget/{uid}');

return new Widget($response->json());
}
}

Сервис использует метод withOptions() для непосредственной настройки параметров Guzzle, но мы могли бы также использовать некоторые удобные методы предоставляемые HTTP клиентом:

$this->app->singleton(ExampleService::class, function (Application $app) {
$client = Http::baseUrl(config('services.example.base_url'))
->timeout(config('services.example.timeout', 10))
->connectTimeout(config('services.example.connect_timeout', 2))
->withToken(config('services.example.token'));

return new ExampleService($client);
});

Или, если вы хотите объединить макросы с сервисами, можно использовать макросы определённые в методе boot() вашего AppServiceProvider:

$this->app->singleton(ExampleService::class, function (Application $app) {
return new ExampleService(Http::github());
});

Конфигурация базового URL

Возможно, вы видели, что базовый URL-адрес по умолчанию включает завершающий /, потому что он обеспечивает большею переносимость в моём варианте, согласно RFC 3986.

Возьмём следующий пример конфигурации сервиса (обратите внимание на base_url по умолчанию):

return [
'example' => [
'base_url' => env('EXAMPLE_BASE_URI', 'https://api.example.com/v1/'),
'token' => env('EXAMPLE_SERVICE_TOKEN'),
'timeout' => env('EXAMPLE_SERVICE_TIMEOUT', 10),
'connect_timeout' => env('EXAMPLE_SERVICE_TIMEOUT', 2),
],
];

Если API имеет префикс пути /v1/ в продакшене, а в stage, возможно, это просто https://stg-api.example.com/; использование завершающего слэша заставляет URL-адрес работать должным образом без изменений кода. В тандеме с настройкой завершающего / обратите внимание, что все вызовы API в моём коде используют относительные пути:

$this->client
->withUrlParameters(['uid' => $uid])
// Example:
// Staging - https://stg-api.example.com/widget/123
// Production - https://api.example.com/v1/widget/123
->get('widget/{uid}');

В документации Guzzle по созданию клиента написано, как различные стили base_uri влияют на разрешение URI.

Предотвращение случайных запросов в тестах

HTTP-клиент Laravel предоставляет отличные инструменты для тестирования, упрощающие написание тестов. Когда я пишу код, взаимодействующий с API, меня беспокоит, что в моих тестах каким-то образом происходят реальные сетевые запросы. Добавьте предотвращение случайных сетевых запросов с помощью HTTP-клиента Laravel:

Http::preventStrayRequests();

Http::fake([
'github.com/*' => Http::response('ok'),
]);

// Запуск тестового кода
// Если какой-либо другой код запускает HTTP-вызов через клиента Laravel
// выбрасывается исключение

На мой взгляд, лучший способ использования preventStrayRequests() — это определить метод SetUp() в тестовых классах взаимодействующих с API. Возможно, его также можно добавить в базовый класс TestCase:

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Http;

abstract class TestCase extends BaseTestCase
{
use CreatesApplication;

public function setUp(): void
{
parent::setUp();

Http::preventStrayRequests();
}
}

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

Обработчики регистрации HTTP событий

HTTP-клиенты Laravel имеют ценные события, которые можно использовать для быстрого доступа к основным этапам жизненного цикла запроса/ответа. На момент написания этой статьи запускаются три события:

Допустим, вы хотите визуализировать каждый HTTP-адрес, к которому приложение отправляет запросы. Мы могли бы легко подключиться к событию RequestSending и сохранять в лог каждый запрос:

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;

class LogRequestSending
{
public function handle(object $event): void
{
Log::debug('HTTP request is being sent.', [
'url' => $event->request->url(),
]);
}
}

Чтобы обработчик событий работал, добавьте в класс EventServiceProvider следующее:

use App\Listeners\LogRequestSending;
use Illuminate\Http\Client\Events\RequestSending;
// ...
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
RequestSending::class => [
LogRequestSending::class,
],
];

Как только всё будет подключено, вы увидите в журнале событий что-то вроде следующего для каждого запроса выполненного с помощью HTTP-клиента:

[2023-03-17 04:06:03] local.DEBUG: HTTP request is being sent. {"url":"https://api.example.com/v1/widget/123"}

Узнавайте больше

В официальной документации Laravel HTTP есть всё, что вам нужно для начала работы. Я надеюсь это статья вдохновила вас и дала советы, которые вы можете использовать в своих приложениях Laravel.

Дополнительные материалы

Предыдущая Статья

CSRF: Обход проверки токена

Следующая Статья

Основы Laravel: Структура каталогов приложения