HTTP-клиент
Дата обновления перевода 2024-07-31
HTTP-клиент
Установка
Компонент HttpClient - это низкоуровневый HTTP-клиент с поддержкой как оберток PHP-стримов, так и cURL. Он предоставляет инструменты для потребления API и поддерживает синхронные и асинхронные операции. Вы можете установить его с помощью:
1
$ composer require symfony/http-client
Базовое использование
Используйте класс HttpClient, чтобы делать
запросы. В фреймворке Symfony, этот класс доступен, как сервис http_client
. Этот
сервис будет автоматически смонтирован при
вводе подсказки HttpClientInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SymfonyDocs
{
public function __construct(
private HttpClientInterface $client,
) {
}
public function fetchGitHubInformation(): array
{
$response = $this->client->request(
'GET',
'https://api.github.com/repos/symfony/symfony-docs'
);
$statusCode = $response->getStatusCode();
// $statusCode = 200
$contentType = $response->getHeaders()['content-type'][0];
// $contentType = 'application/json'
$content = $response->getContent();
// $content = '{"id":521583, "name":"symfony-docs", ...}'
$content = $response->toArray();
// $content = ['id' => 521583, 'name' => 'symfony-docs', ...]
return $content;
}
}
Tip
HTTP-клиент взаимодействует со многими распространенными абстракциями HTTP клиентов в PHP. Вы также можете использовать любую из этих абстракций, чтобы извлечь пользу из автомонтирований. См. Взаимосовместимость, чтобы узнать больше.
Конфигурация
HTTP-клиент содержит множество опций, которые могут вам понадобиться для полного контроля над тем, как выполняется запрос, включая предварительное разрешение DNS, параметры SSL, фиксацию публичных ключей и др. Они могут быть определены глобально в конфигурации (чтобы применить ко всем запросам), и к каждому запросу отдельно (что переоепределяет любую глобальную конфигурацию).
Вы можете сконфигурировать глобальные опции используя опцию default_options
:
1 2 3 4 5
# config/packages/framework.yaml
framework:
http_client:
default_options:
max_redirects: 7
Вы также можете использовать метод withOptions(), чтобы извлечь новый экземпляр клиента с новыми опциями по умолчанию:
1 2 3 4 5
$this->client = $client->withOptions([
'base_uri' => 'https://...',
'headers' => ['header-name' => 'header-value'],
'extra' => ['my-key' => 'my-value'],
]);
В качестве альтернативы, класс HttpOptions предоставляет большинство доступных опций с геттерами и сеттерами с подсказкой типа:
1 2 3 4 5 6 7 8 9
$this->client = $client->withOptions(
(new HttpOptions())
->setBaseUri('https://...')
// заменяет *все* заголовки одномоментно, и удаляет заголовки, которые вы не предоставляете
->setHeaders(['header-name' => 'header-value'])
// установить или заменить один заголовок, используя addHeader()
->setHeader('another-header-name', 'another-header-value')
->toArray()
);
7.1
Метод setHeader() был представлен в Symfony 7.1.
Некоторые опции, описанные в этом руководстве:
- Аутентификация
- Параметры строки запроса
- Заголовки
- Перенаправления
- Повторная попытка неудачных запросов
- HTTP-прокси
- Использование шаблонов URI
Посмотрите полный справочник конфигурации http_client , чтобы узнать о всех опциях.
HTTP-клиент также имеет одну опцию конфигурации под названием
max_host_connections
, эта опция не может быть переопределена запросом:
1 2 3 4 5
# config/packages/framework.yaml
framework:
http_client:
max_host_connections: 10
# ...
Определение клиента
Часто бывает так, что некоторые опции HTTP-клиента зависят от URL запроса (наприер, вы должны установить некоторые заголовки при запросе к GitHub API, но не к другим хостам). Если это ваш случай, компонент предоставляет определенных клиентов (используя ScopingHttpClient) для автоконфигурации HTTP-клиента на основе запрошенного URL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# config/packages/framework.yaml
framework:
http_client:
scoped_clients:
# только запросы, совпадающие с определением, будут использовать эти опции
github.client:
scope: 'https://api\.github\.com'
headers:
Accept: 'application/vnd.github.v3+json'
Authorization: 'token %env(GITHUB_API_TOKEN)%'
# ...
# использование base_uri, относительных URL (например, request("GET", "/repos/symfony/symfony-docs"))
# будет по умолчанию в этих опциях
github.client:
base_uri: 'https://api.github.com'
headers:
Accept: 'application/vnd.github.v3+json'
Authorization: 'token %env(GITHUB_API_TOKEN)%'
# ...
Вы можете определить несколько определений, чтобы каждый набор опций добавлялся
только в случае, если запрошенный URL совпадает с одним из регулярных выражений,
установленных опцией scope
.
Если вы используете определенных клиентов в фреймворке Symfony, вы должны использовать любые из методов, определенных Symfony, чтобы выбрать конкретных сервис . Каждый клиент имеет уникальный сервис, названный по его конфигурации.
Каждый определенный клиент также определяет соответствующе названный
псевдоним автомонтирования. Если вы, к примеру, используете
Symfony\Contracts\HttpClient\HttpClientInterface $githubClient
в качестве
типа и имени аргумента, автомонтирование внедрит сервис github.client
в
ваши автоматически смонтированные классы.
Note
Прочтите документацию опции base_uri , чтобы узнать правила, применяемые при слиянии относительных URL в базовый URI определенного клиента.
Запросы
HTTP-клиент предоставляет единственный метод request()
для выполнения всех
видов HTTP-запросов:
1 2 3 4 5 6 7 8 9 10 11
$response = $client->request('GET', 'https://...');
$response = $client->request('POST', 'https://...');
$response = $client->request('PUT', 'https://...');
// ...
// вы можете добавить опции запроса (или переопределить глобальные), используя 3й аргумент
$response = $client->request('GET', 'https://...', [
'headers' => [
'Accept' => 'application/json',
],
]);
Ответы всегда асинхронны, чтобы вызов метода возвращался немедленно, вместо ожидания получения ответа:
1 2 3 4 5 6 7 8 9
// выполнение кода продолжается немедленно; он не ждет получения ответа
$response = $client->request('GET', 'http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso');
// получение заголовков ответа ждет их прибытия
$contentType = $response->getHeaders()['content-type'][0];
// попытка получить содержание ответа заблокирует выполнение до
// момента получения полного содержания ответа
$content = $response->getContent();
Этот компонент также поддерживает потоковую передачу ответов для полностью асинхронных приложений.
Аутентификация
HTTP-клиент поддерживает различные механизмы аутентификации. Они могут быть определены глобально в конфигурации (чтобы применить ко всем запросам) и к каждому запросу отдельно (что переопределяет любую глобальную аутентификацию):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/framework.yaml
framework:
http_client:
scoped_clients:
example_api:
base_uri: 'https://example.com/'
# Базовая HTTP аутентификация
auth_basic: 'the-username:the-password'
# Аутентификация HTTP Bearer (также называемая аутентификацией токена)
auth_bearer: the-bearer-token
# Аутентификация Microsoft NTLM
auth_ntlm: 'the-username:the-password'
1 2 3 4 5 6
$response = $client->request('GET', 'https://...', [
// используйте другую базовую HTTP аутентификацию только для этого запроса
'auth_basic' => ['the-username', 'the-password'],
// ...
]);
Note
Механизм аутентификации NTLM требует использования транспорта cURL.
Используя HttpClient::createForBaseUri()
, мы гарантируем, что
идентификационные данные авторизации не будут отправлены никаким хостам,
кроме https://example.com/.
Параметры строки запроса
Вы можете либо добавить их в начало запрошенного URL вручную, либо определить их
в качестве ассоциативного массива через опцию query
, которая будет объединена
с URL:
1 2 3 4 5 6 7 8
// создает запрос HTTP GET к https://httpbin.org/get?token=...&name=...
$response = $client->request('GET', 'https://httpbin.org/get', [
// эти значения автоматически шифруются перед добавлением их в URL
'query' => [
'token' => '...',
'name' => '...',
],
]);
Заголовки
Используйте опцию headers
, чтобы определить заголовки, по умолчанию добавленные
ко всем запросам:
1 2 3 4 5 6
# config/packages/framework.yaml
framework:
http_client:
default_options:
headers:
'User-Agent': 'My Fancy App'
Вы также можете установить новые заголовки, или переопределить установленные по умолчанию, для конкретного запроса:
1 2 3 4 5 6 7
// этот заголовок включен только в этот запрос и переопределяет значение
// того же заголовка, если он определен глобально HTTP-клиентом
$response = $client->request('POST', 'https://...', [
'headers' => [
'Content-Type' => 'text/plain',
],
]);
Загрузка данных
Этот компонент предоставляет несколько методов загрузки данных, используя опцию
body
. Вы можете использовать обычные строки, замыкания, итерации и источники,
и они будут автоматически обработаны при создании запросов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$response = $client->request('POST', 'https://...', [
// определение данных, используя обычную строку
'body' => 'raw data',
// определение данных, используя массив параметров
'body' => ['parameter1' => 'value1', '...'],
// использование замыкания для генерирования загруженных данных
'body' => function (int $size): string {
// ...
},
// использование истоничка для получения данных из него
'body' => fopen('/path/to/file', 'r'),
]);
При загрузке данных с помощью метода POST
, если вы не хотите определять HTTP
заголовок Content-Type
ясно, Symfony предполагает, что вы загружаете данные формы,
и добавлят обязательный заголовок 'Content-Type: application/x-www-form-urlencoded'
за вас.
Когда опция body
установлена, как замыкание, она будет вызвана несколько раз,
прежде чем вернет пустую строку, сигнализирующую окончание тела. Каждый раз, замыкание
должно вернуть строку, меньшую, чем была запрошена в качестве аргумента.
Генератор, или любое Traversable
, также могут быть использованы вместо замыкания.
Tip
При загрузке полезной нагрузки JSON, используйте опцию json
вместо body
.
Заданное содержание будет автоматически JSON-зашифровано, и запрос будет также
автоматически добавлять Content-Type: application/json
:
1 2 3 4 5
$response = $client->request('POST', 'https://...', [
'json' => ['param1' => 'value1', '...'],
]);
$decodedPayload = $response->toArray();
Чтобы отправить форму с загруженными файлами, передайте обработчик файла в опцию body
:
1 2
$fileHandle = fopen('/path/to/the/file', 'r');
$client->request('POST', 'https://...', ['body' => ['the_file' => $fileHandle]]);
По умолчанию этот код заполняет имя файла и тип содержания данными открытого файла, но вы можете сконфигурировать оба параметра с помощью конфигурации потоковой передачи PHP:
1 2
stream_context_set_option($fileHandle, 'http', 'filename', 'the-name.txt');
stream_context_set_option($fileHandle, 'http', 'content_type', 'my/content-type');
Tip
При использовании многомерных массивов, класс
FormDataPart
автоматически добавляет [key]
в начало имени поля:
1 2 3 4 5 6 7 8 9
$formData = new FormDataPart([
'array_field' => [
'some value',
'other value',
],
]);
$formData->getParts(); // Возвращает два экземпляра TextPart
// с именами "array_field[0]" и "array_field[1]"
Это поведение можно обойти, использоуя следующую структуру массива:
1 2 3 4 5 6 7
$formData = new FormDataPart([
['array_field' => 'some value'],
['array_field' => 'other value'],
]);
$formData->getParts(); // Возвращает два экземпляра TextPart
// оба с именем "array_field"
По умолчанию, HttpClient стримит содержание тела при их загрузке. Это может
работать не со всеми серверами, что приведет к HTTP статус-коду 411 ("Необходимая длина"),
так как нет заголовка Content-Length
. Решение - превратить тело в строку с помощью
следующего метода (что увеличит потребление памяти, если потоки большие):
1 2 3 4
$client->request('POST', 'https://...', [
// ...
'body' => $formData->bodyToString(),
]);
Если вам нужно добавить пользовательский HTTP-заголовок к загрузке, вы можете:
1 2
$headers = $formData->getPreparedHeaders()->toArray();
$headers[] = 'X-Foo: bar';
Куки
HTTP-клиент, предоставленный данным компонентом, не фиксирует состояние, но обработка куки требует хранилища фиксации состояния (потому как ответы могут обновлять куки, и они должны быть использованы для вытекающих запросов). Поэтому этот компонент не обрабатывает куки автоматически.
Вы можете либо обрабатывать куки самостоятельно, используя HTTP-заголовокё
Cookie
, или использовать компонент BrowserKit,
который предоставляет эту функцию и безупречно интегрируется с компонентом HttpClient.
Вы можете либо отправить куки с компонентом BrowserKit ,
который безупречно интегрируется с компонентом HttpClient, либо вручную, установив HTTP-заголовок
Cookie
следующим образом:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\Cookie;
$client = HttpClient::create([
'headers' => [
'Cookie' => new Cookie('flavor', 'chocolate', strtotime('+1 day')),
// вы также можете передать содержание куки в качестве строки
'Cookie' => 'flavor=chocolate; expires=Sat, 11 Feb 2023 12:18:13 GMT; Max-Age=86400; path=/'
],
]);
Перенаправления
По умолчанию, HTTP-клиент следует перенаправлениям (максмум 20-ти), при
выполнении запроса. Используйте настройку max_redirects
, чтобы сконфигурировать
данное поведение (если количество перенаправлений больше, чем сконфигурированное
значение, вы получите
RedirectionException):
1 2 3 4
$response = $client->request('GET', 'https://...', [
// 0 означает не следовать перенаправлениям
'max_redirects' => 0,
]);
Повторная попытка неудачных запросов
Иногда, запросы терпят неудачу из-за проблем с сетью или временных ошибок сервера. HttpClient Symfony позволяет повторно пытаться обработать неудачные запросы автоматически, используя опцию retry_failed .
По умолчанию, неудачные запросы имеют до трех повторных попыток с растущим промежутком
между попытками (первая попытка = 1 секунда; третья попытка: 4 секунды) и только для
следующих HTTP статус-кодов: 423
, 425
, 429
, 502
и 503
при
использовании любого HTTP-метода, и для 500
, 504
, 507
и 510
при использовании
HTTP метода idempotent.
Просмотрите полный список конфигурируемых опций retry_failed , чтобы узнать, как настроить каждую из них так, чтобы она соответствовала нуждам вашего приложения.
При использовании HttpClient вне приложения Symfony, используйте класс RetryableHttpClient, чтобы обернуть вашего изначального HTTP-клиента:
1 2 3
use Symfony\Component\HttpClient\RetryableHttpClient;
$client = new RetryableHttpClient(HttpClient::create());
RetryableHttpClient использует RetryStrategyInterface, чтобы решить, следует ли повторить запрос, и определить время ожидания между каждой повторной попыткой.
Повторная попытка нескольких базовых URI
RetryableHttpClient
может быть сконфигурирован для использования нескольких базовых URI. Эта
возможность обеспечивает повышенную гибкость и надежность при выполнении HTTP-запросов. Передайте
массив базовых URI в качестве опции base_uri
при выполнении запроса:
1 2 3 4 5 6 7 8
$response = $client->request('GET', 'some-page', [
'base_uri' => [
// первый запрос будет использовать этот базовый URI
'https://example.com/a/',
// если первый запрос потерпит неудачу, будет использован следующий базовый URI
'https://example.com/b/',
],
]);
Если количество повторных попыток превышает количество базовых URI, то последний базовый URI будет использоваться для оставшихся повторных попыток.
Если вы хотите перетасовать порядок базовых URI при каждой попытке повтора, вложите
базовые URI, которые вы хотите перетасовать, в дополнительный массив:
1 2 3 4 5 6 7 8 9 10 11
$response = $client->request('GET', 'some-page', [
'base_uri' => [
[
// один рандомный URI из этого массива будет использован для первого запроса
'https://example.com/a/',
'https://example.com/b/',
],
// невложенные базовые URI используются по порядку
'https://example.com/c/',
],
]);
Эта функция позволяет использовать более рандомизированный подход к обработке повторных попыток, уменьшая вероятность повторного обращения к одному и тому же неудачному базовому URI.
Используя вложенный массив для базового URI, вы можете использовать эту функцию
для распределения нагрузки между многими узлами в кластере серверов.
Вы также можете сконфигурировать массив базовых URI с помощью метода withOptions()
:
1 2 3 4
$client = $client->withOptions(['base_uri' => [
'https://example.com/a/',
'https://example.com/b/',
]]);
HTTP-прокси
По умолчанию, этот компонент уважает стандартные переменные окружения, определенные вашей ОС, для направления HTTP-траффика через ваш локальный прокси. Это означает, что обычно тут нечего конфигурировать, для работы клиента с прокси, при условии, что эти переменные окружения сконфигурированы правильно.
Вы все еще можете устанавливать или переопределять эти настройки используя опции
proxy
и no_proxy
:
proxy
должна быть установлена как URL проксиhttp://...
no_proxy
отключает прокси для списка хостов, разделенных запятыми, доступ к которым не нужен.
Обратный вызов прогресса
Предоставив вызываемое опции on_progress
, вы можете отследить загрузки/выгрузки
по мере их завершения. Этот обратный вызов гарантированно будет вызван при разрешении
DNS, поступлении заголовков и завершению работы; кроме того, он вызывается когда загружаются
или выгрудаются новые данные, как минимум раз в секунду:
1 2 3 4 5 6 7
$response = $client->request('GET', 'https://...', [
'on_progress' => function (int $dlNow, int $dlSize, array $info): void {
// $dlNow - количество уже скачанных байтов
// $dlSize - общий размер загрузки, или -1, если это неизвестно
// $info - это то, что вернет $response->getInfo() в данное конкретное время
},
]);
Все исключения, вызванные обратным вызовом, будут обернуты в экземпляр TransportExceptionInterface и прервут запрос.
Сертификаты HTTPS
HttpClient использует хранилище сертификатов системы для валидации SSL-сертификатов (а браузеры используют собственные хранилища). При использовании самоподписанных сертивикатов во время разработки, рекомендуется создавать собственный авторитет сертификатов (CA) и добавлять его в хранилище вашей системы.
Как вариант, вы также можете отключить verify_host
и verify_peer
(см.
http_client config reference ), но это не рекомендуется
в производстве.
Работа с SSRF (подделка запросов стороны сервера)
SSRF позволяет хакеру вынудить приложение бекэнда делать HTTP-запросы к произвольному домену. Такие атаки также могут быть нацелены на внутренние хостинги и IP атакованного сервера.
Если вы используете HttpClient вместе с предоставленными пользователями URI, скорее всего хорошей идеей будет облачить его в NoPrivateNetworkHttpClient. Это гарантирует, что локальные сети будут недоступны HTTP-клиенту:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
$client = new NoPrivateNetworkHttpClient(HttpClient::create());
// ничего не меняется при запросе к публичным сетям
$client->request('GET', 'https://example.com/');
// однако, все запросы к приватным сетям теперь блокируются по умолчанию
$client->request('GET', 'http://localhost/');
// второй необязательный аргумент определяет сети для блокировки
// в этом примере, запросы с 104.26.14.0 до 104.26.15.255 приведут к исключению,
// но все другие запросы, включая другие внутренние сети, будут позволены
$client = new NoPrivateNetworkHttpClient(HttpClient::create(), ['104.26.14.0/23']);
Профилирование
Когда вы используете TraceableHttpClient, содержание ответа будет сохраняться в памяти и может истощить её.
Вы можете отключить это поведение, установив опцию extra.trace_content
как false
в ваших запосах:
1 2 3
$response = $client->request('GET', 'https://...', [
'extra' => ['trace_content' => false],
]);
Эта настройка не повлияет на других клиентов.
Использование шаблонов URI
UriTemplateHttpClient предоставляет клиента, который облегчает использование шаблонов URI, как описано в RFC 6570:
1 2 3 4 5 6 7 8 9
$client = new UriTemplateHttpClient();
// это сделает запрос к URL http://example.org/users?page=1
$client->request('GET', 'http://example.org/{resource}{?page}', [
'vars' => [
'resource' => 'users',
'page' => 1,
],
]);
Прежде чем использовать шаблоны URI в своих приложениях, необходимо установить сторонний пакет, который расширит шаблоны URI и превратит их в URL:
1 2 3 4 5
$ composer require league/uri
# Symfony также поддерживает следующие пакеты шаблонов URI:
# composer require guzzlehttp/uri-template
# composer require rize/uri-template
При использовании этого клиента в контексте фреймворка все существующие HTTP-клиенты декорируются с помощью UriTemplateHttpClient. Это означает, что функция шаблона URI включена по умолчанию для всех HTTP-клиентов, которых вы можете использовать в своем приложении.
Вы можетесконфигурировать переменные, которые будут глобально заменяться во всех шаблонах URI вашего приложения:
1 2 3 4 5 6
# config/packages/framework.yaml
framework:
http_client:
default_options:
vars:
- secret: 'secret-token'
Если вы хотите определить свою собственную логику для работы с переменными шаблонов URI,
это можно сделать, переопределив псевдоним http_client.uri_template_expander
. Ваш
сервис должен быть вызываемым.
Производительность
Компонент создан для максимальной HTTP-производительности. Он совместим с HTTP/2 и с созданием пересекающихся асинхронных потоковых и мультиплексных запросов/ответов. Даже при регулярных синхронных вызовах, он позволяет оставлять соединения с удаленными хостами открытыми между запросами, что улучшает производительность, сохраняя повторяющиеся DNS разрешение, SSL переговоры, и т.д. Чтобы пользоваться всеми этими преимуществами, неохобимо расширение cURL.
Подключение поддержки cURL
Этот компонент поддерживает как нативные PHP-потоки, так и cURL, чтобы делать HTTP-запросы. Хотя они взаимозаменяемы и предоставляют одинаковые функции, включая пересекающиеся запросы, HTTP/2 поддерживается только при использовании cURL.
Note
Для использования AmpHttpClient, должен быть установлен пакет amphp/http-client.
Метод create()
выбирает транспорт cURL, если включено PHP-расширение cURL. Если cURL не удалось найти
или он слишком устарел, в качестве резерва используется AmpHttpClient
. Наконец, если
AmpHttpClient
недоступен, в качестве резерва используются потоки PHP.
Если вы предпочитаете выбирать транспорт явно, используйте следующие классы
для создания клиента:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\HttpClient\AmpHttpClient;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;
// использует нативные PHP-потоки
$client = new NativeHttpClient();
// использует PHP-расширение cURL
$client = new CurlHttpClient();
// использует клиента из пакета `amphp/http-client`
$client = new AmpHttpClient();
При использовании этого компонента в полностековом приложении Symfony, данное поведение невозможно сконфигурировать, и cURL будет использован автоматически, если PHP-расширение cURL установлено и подключено. В других случаях будут использованы нативные PHP-потоки.
Конфигурация опций CurlHttpClient
PHP позволяет конфигурировать множество опций cURL через функцию curl_setopt. Для того, чтобы компонент был более портативным без использования cURL, CurlHttpClient использует только некоторые из этих опций (и они игнорируются во всех других клиентах).
Добавьте опцию extra.curl
в вашей конфигурации, чтобы передать эти дополнительные опции:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\HttpClient\CurlHttpClient;
$client = new CurlHttpClient();
$client->request('POST', 'https://...', [
// ...
'extra' => [
'curl' => [
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V6,
],
],
]);
Note
Некоторые опции cURL невозможно переопределить (например, из-за безопасности потоков) и вы получит исключение при попытке их переопределить.
HTTP-сжатие
HTTP-заголовок Accept-Encoding: gzip
добавляется автоматически, если:
- При использовании клиента cURL: cURL был скомпилирован с поддержкой ZLib (см.
php --ri curl
) - При использовании нативного клиента HTTP: устанавливается PHP-расширение Zlib
Если сервер не отвечает ответом gzip, он дешифруется прорзрачно. Чтобы отключить
сжатие HTTP, отправьте HTTP-заголовок Accept-Encoding: identity
.
Шифрование трансфера кусками включается автоматически, если это поддеживает и ваше время прогона PHP и удалённый сервер.
Caution
Если вы установите значение Accept-Encoding
, как, например, gzip
, вам придется
обрабатывать распаковку самостоятельно.
Поддержка HTTP/2
При запросе URL https
URL, HTTP/2 включается по умолчанию, если установлен
один из следующих инструментов:
- Пакет libcurl версии 7.36 или выше;
- Пакет Packagist amphp/http-client версии 4.2 или выше.
Чтобы форсировать HTTP/2 для URL http
, вам нужно ясно его включить через
опцию http_version
:
1 2 3 4 5
# config/packages/framework.yaml
framework:
http_client:
default_options:
http_version: '2.0'
Поддержка для PUSH HTTP/2 работает сразу после установки, если libcurl >= 7.61 используется с PHP >= 7.2.17 / 7.3.4: пуш-ответы помещаются во временный кеш и используются, когда запускается последующий запрос для соответствующих URL.
Обработка ответов
Ответ, который возвращается всеми HTTP-клиентами, - это объект типа ResponseInterface, предоставляющий следующие методы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
$response = $client->request('GET', 'https://...');
// получает HTTP статус-код ответа
$statusCode = $response->getStatusCode();
// получает HTTP-заголовки в виде строки[][] с именами заголовков в нижнем регистре
$headers = $response->getHeaders();
// получает тело ответа в виде строки
$content = $response->getContent();
// приводит JSON-содержание ответа в PHP-массив
$content = $response->toArray();
// приводит содержание ответа в источник PHP-потока
$content = $response->toStream();
// отменяет запрос/ответ
$response->cancel();
// возвращает информацию, исходяющую из слоя транспорта, вроде "response_headers",
// "redirect_count", "start_time", "redirect_url", и т.д.
$httpInfo = $response->getInfo();
// вы также можете получить индивидуальную информацию
$startTime = $response->getInfo('start_time');
// например, это вернет URL финального ответа (разрешая перенаправления при необходимости)
$url = $response->getInfo('url');
// возвращает детальные логи о запросах и ответах HTTP-транзакции
$httpLogs = $response->getInfo('debug');
// специальный инфо-объект "pause_handler" является вызываемым, которое позволяет отсрочить запрос
// на заданное количество секунд; это позволяет вам отсрочить повторные попытки, дроссельные потоки и т.д.
$response->getInfo('pause_handler')(2);
Note
$response->toStream()
является частью StreamableInterface.
Note
$response->getInfo()
является неблокирующим: он возвращает живую информацию
об ответе. Некоторая из них может быть еще неизвестна (к примеру, http_code
),
во время ее вызова.
Потоковые ответы
Вызовите метод stream()
HTTP-клиента, чтобы получать куски ответа
последовательно, а не ждать ответа целиком:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$url = 'https://releases.ubuntu.com/18.04.1/ubuntu-18.04.1-desktop-amd64.iso';
$response = $client->request('GET', $url);
// Ответы ленивы: этот код выполняется сразу после получения заголовков
if (200 !== $response->getStatusCode()) {
throw new \Exception('...');
}
// получите содержание ответа кусками и сохраните их в файл
// куски ответа реализуют Symfony\Contracts\HttpClient\ChunkInterface
$fileHandler = fopen('/ubuntu.iso', 'w');
foreach ($client->stream($response) as $chunk) {
fwrite($fileHandler, $chunk->getContent());
}
Note
По умолчанию, тело ответов text/*
, JSON и XML буферизуются в локальном
потоке php://temp
. Вы можете контролировать это поведение, используя опцию
buffer
: установите ее как true
/false
чтобы включить/отключить буферизацию,
или как замыкание, которое должно вернуть то же самое, основываясь на полученных
в качестве аргументы заголовках.
Отмена ответов
Чтобы прервать запрос (например, потому что он не был выполнен вовремя, или если вы хотите извлечь только первые байти информации и т.д.), вам нужно либо использовать cancel():
1
$response->cancel();
Либо вызвать исключение из прогрессивного обратного вызова:
1 2 3 4 5 6 7
$response = $client->request('GET', 'https://...', [
'on_progress' => function (int $dlNow, int $dlSize, array $info): void {
// ...
throw new \MyException();
},
]);
Исключение будет обернуто в экземпляр TransportExceptionInterface и прервет запрос.
В случае, если ответ был отменен используя $response->cancel()
,
$response->getInfo('canceled')
вернет true
.
Обработка исключений
Существует три типа исключений, все из которых реализуют ExceptionInterface:
- Исключения, реализующие HttpExceptionInterface, вызываются, когда ваш код не обрабатывает статус-коды в диапазоне 300-599.
- Исключения, реализующие TransportExceptionInterface, вызываются, когда возникает ошибка низлежащего уровня.
- Исключения, реализующие DecodingExceptionInterface, вызываются, когда тип содержания не может быть зашифрован в ожидаемый вид.
Когда HTTP статус-код ответа находится в диапазоне 300-599 (т.е. 3xx,
4xx или 5xx) ваш код должен его обработать. Если вы этого не сделаете, методы
getHeaders()
, getContent()
и toArray()
вызовут соответствующие
исключения, все из которых реализуют HttpExceptionInterface:
Чтобы избежать этого исключения и самостоятельно разобраться со статус-кодами 300-599,
передайте false
в качестве необязательного аргумента каждому из этих методов, например,
$response->getHeaders(false);
.
Если вы вообще не вызовете ни один из этих 3 методов, исключение все равно будет вызываться
при деструкции объекта $response
.
Вызова $response->getStatusCode()
достаточно для отключения такого поведения
(но затем не забывайте самостоятельно проверять статус-код).
Хотя ответы ленивы, их деструктор будет всегда ждать возвращения заголовков. Это означает, что следующий запрос будет выполнен; и если, к примеру, вернется 404, будет вызвано исключение:
1 2 3 4
// так как возвращенное значение не назначено переменной, деструктор
// возвращенного ответа будет вызван немедленно, и вызовет исключение, если
// статус-код будет в диапазоне 300-599
$client->request('POST', 'https://...');
Это, в свою очередь, означает, что неопределенные ответы будут откатываться до синхронных запросов. Если вы хотите сделать эти запросы параллельными, вы можете хранить их соответствующие ответы в массиве:
1 2 3 4 5 6 7 8
$responses[] = $client->request('POST', 'https://.../path1');
$responses[] = $client->request('POST', 'https://.../path2');
// ...
// Эта строчка запустит деструктор всех ответов, хранящихся в массиве;
// они будут выполнены параллельно, а исключение будет вызвано в случае,
// если будет возвращен статус-код в диапазоне 300-599
unset($responses);
Это поведение, предоставленное во время деструкции, является частью безаварийного
проектирования компонента. Ни одна ошибка не будет незамеченной: если вы не напишете
код для обработки ошибок, исключения уведомят вас при необходимости. С другой стороны,
если вы напишите код обработки ошибок (вызвав $response->getStatusCode()
), вы откажетесь
от этих резервных механизмов, так как деструктору не будет что делать.
Параллельные запросы
Благодаря тому, что ответы ленивы, запросы всегда обрабатываются параллельно. В достаточно быстрой сети, следующий код делает 379 запросов менее, чем за полсекунды, когда используется cURL:
1 2 3 4 5 6 7 8 9 10
$responses = [];
for ($i = 0; $i < 379; ++$i) {
$uri = "https://http2.akamai.com/demo/tile-$i.png";
$responses[] = $client->request('GET', $uri);
}
foreach ($responses as $response) {
$content = $response->getContent();
// ...
}
Как вы можете увидеть в первом цикле "for", запросы выпускаются, но еще не потребляются. Это фокус параллельности: запросы должны быть вначале отправлены, а прочитаны позже. Это позволит клиенту мониторить все ожидающие запросы в то время как ваш код ждет конкретного, как делается в каждой итерации вышенаписанного цикла "foreach".
Note
Максимальное количество параллельных запросов, которое вы можете выполнить, зависит от ресурсов вашей машины (например, ваша ОС может ограничивать количество одновременных чтений файла, хранящего файл сертификатов). Делайте запросы партиями, чтобы избежать проблем.
Мультиплексирование ответов
Если вы снова посмотрите на отрывок кода наверху, ответы читаются в порядке запросов. Но может второй ответ пришел до первого. Полностью асинхронные опарции требуют возможности обработки ответов в любом порядке, в котором они возвращаются.
Для того, чтобы сделать это, stream() принимает список ответов для мониторинга. Как было упомянуто ранее , этот метод создает куски ответов по мере их поступления из сети. Заменив "foreach" в отрывке на это, код станет полностью асинхронным:
1 2 3 4 5 6 7 8 9 10 11 12
foreach ($client->stream($responses) as $response => $chunk) {
if ($chunk->isFirst()) {
// заголовки $response только что прибыли
// $response->getHeaders() теперь является неблокирующим вызовом
} elseif ($chunk->isLast()) {
// полное содержание $response только что было завершено
// $response->getContent() теперь является неблокирующим вызовом
} else {
// $chunk->getContent() вернет кусок
// тела ответа, который только что прибыл
}
}
Tip
Используйте опцию user_data
в сочетании с $response->getInfo('user_data')
для отслеживания идентичности ответа в ваших циклах foreach.
Работа с тайм-аутами соединения
Этот компонент позволяет работать как с таймаутами запросов, так и ответов.
Тайм-аут может произойти когда, к примеру, разрешение DNS занимает слишком много
времени, когда соединение TCP не может быть открыто в заданное время, или когда
содержание ответа слишком надолго находится в паузе. Это может быть сконфигурировано
с помощью опции запроса timeout
:
1 2 3
// Будет выпущен TransportExceptionInterface, если ничего
// не произойдет за 2.5 секунды при доступе из $response
$response = $client->request('GET', 'https://...', ['timeout' => 2.5]);
Настройка PHP ini default_socket_timeout
используется, если опция не установлена.
Опция может быть переопределена с использованием второго аргумента метода stream()
.
Это позволяет мониторить несколько ответов одноврменно и применять таймаут ко всем,
находящимся в группе. Если все ответы станут неактивными на заданное количество времени,
метод создаст специальный кусок, чей isTimeout()
вернет true
:
1 2 3 4 5
foreach ($client->stream($responses, 1.5) as $response => $chunk) {
if ($chunk->isTimeout()) {
// $response был просрочен больше, чем на 1.5 секунды
}
}
Тайм-аут не обязательно является ошибкой: вы можете решить снова запустить поток ответа и получить оставшееся содержание, которое может вернуться в новом таймауте, и т.д.
Tip
Передача 0
в качестве таймаута позволяет мониторить ответы неблокирующим образом.
Note
Тайм-ауты контролируют сколько кто-то готов ждать, во время бездействия HTTP-транзакции. Большие ответы могут занимать столько времени, сколько им нужно для завершения, если они остаются активными во время передачи и не делают паузу дольше, чем указано.
Используйте опцию max_duration
, чтобы ограничить время, которое может занимать
полный запрос/ответ.
Работа с ошибками сети
Ошибки сети (сломанные трубы, неудача разрешения DNS и т.д.) вызываются как экземпляры TransportExceptionInterface.
Для начала, вам не обязательно обрабатывать их: вы можете позволить ошибкам собираться в вашем общем стеке обработке исключений, и это может быть нормально в большинстве случаев использования.
Если же вы хотите обработать их, вот, что вам нужно знать:
Чтобы поймать ошибки, вам нужно обернуть вызовы в $client->request()
, но также
вызовы к любым методам возвращенных ответов. Так как ответы ленивы, ошибки сети могут
возникать и во время вызова, к примеру, getStatusCode()
:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
// ...
try {
// обе строчки могут потенциально вызвать
$response = $client->request(...);
$headers = $response->getHeaders();
// ...
} catch (TransportExceptionInterface $e) {
// ...
}
Note
Так как $response->getInfo()
является неблокирующим, он не должен вызывать ошибку.
При мульиплексировании ответов, вы можете работать с ошибками для конкретных потоков, отлавливая TransportExceptionInterface в цикле foreach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
foreach ($client->stream($responses) as $response => $chunk) {
try {
if ($chunk->isTimeout()) {
// ... решите, что делать, когда произойдет таймаут
// если вы хотите остановить ответ, который вызвал таймаут, не забудьте
// вызывать $response->cancel(), иначе деструктор ответа
// попробует завершить его еще раз
} elseif ($chunk->isFirst()) {
// если вы хотите проверить статус-код, вы должны сделать это по прибытию
// первого куска, используя $response->getStatusCode();
// если вы этого не сделаете, это может запустить HttpExceptionInterface
} elseif ($chunk->isLast()) {
// ... сделайте что-то с $response
}
} catch (TransportExceptionInterface $e) {
// ...
}
}
Кеширование запросов и ответов
Данный компонент предоставляет декоратор CachingHttpClient, который позволяет кешировать ответы и подавать их из локального хранилища по следующим запросам. Реализация по сути использует преимущества класса HttpCache, поэтому в вашем приложении должен быть установлен компонент HttpKernel:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\HttpClient\CachingHttpClient;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpKernel\HttpCache\Store;
$store = new Store('/path/to/cache/storage/');
$client = HttpClient::create();
$client = new CachingHttpClient($client, $store);
// это не дойдет до сети, если источник уже находится в кеше
$response = $client->request('GET', 'https://example.com/cacheable-resource');
CachingHttpClient принимает третий аргумент, чтобы установить опции HttpCache.
Ограничение количества запросов
Этот компонент предоставляет декоратор ThrottlingHttpClient, который позволяет ограничить количество запросов в течение определенного периода времени.
Реализация использует класс LimiterInterface за кулисами, поэтому компонент Rate Limiter должен быть установить в вашем приложении:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\ThrottlingHttpClient;
use Symfony\Component\RateLimiter\LimiterInterface;
$rateLimiter = ...; // $rateLimiter является экземпляром Symfony\Component\RateLimiter\LimiterInterface
$client = HttpClient::create();
$client = new ThrottlingHttpClient($client, $rateLimiter);
$requests = [];
for ($i = 0; $i < 100; $i++) {
$requests[] = $client->request('GET', 'https://example.com');
}
foreach ($requests as $request) {
// В зависимости от политики ограничений, вызовы будут отсрочены
$output->writeln($request->getContent());
}
7.1
ThrottlingHttpClient был представлен в Symfony 7.1.
Потребление событий, отправленных сервером
События, отправленные сервером - это интернет-стандарт для загрузки данных на
веб-страницы. Его API JavaScript построен вокруг объекта EventSource, который слушает
события, отправленные с некоторого URL. События - это потоки данных (поданные с MIME-типом
text/event-stream
) со следующим форматом:
1 2 3 4 5 6
data: Это первое сообщение.
data: Это второе сообщение, оно
data: имеет две строчки.
data: Это третье сообщение.
HTTP-клиент Symfony предоставляет реализацию EventSource для потребления этих
событий, отправленных сервером. Используйте EventSourceHttpClient,
чтобы обернуть ваш HTTP-клиент, открыть соединение с сервером, который отвечает
типом содержания text/event-stream
, и потреблять поток следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\EventSourceHttpClient;
// второй необязательный аргумент - время повторного соединения в секундах (по умолчанию = 10)
$client = new EventSourceHttpClient($client, 10);
$source = $client->connect('https://localhost:8080/events');
while ($source) {
foreach ($client->stream($source, 2) as $r => $chunk) {
if ($chunk->isTimeout()) {
// ...
continue;
}
if ($chunk->isLast()) {
// ...
return;
}
// это специальный кусок ServerSentEvent, содержащий отправленное сообщение
if ($chunk instanceof ServerSentEvent) {
// сделайте что-то с событием сервера ...
}
}
}
Tip
Если вы знаете, что содержание ServerSentEvent
находится в формате JSON, вы можете
использовать метод getArrayData(),
чтобы напрямую получить дешифрованный JSON в виде массива.
Взаимосовместимость
Компонент взаимосовместим с четырьмя разными абстракциями для HTTP-клиентов: Symfony Contracts, PSR-18, HTTPlug v1/v2 и нативными PHP-потоками. Если ваше приложение использует библиотеки, которые нуждаются в любой из них, компонент совместим с ними всеми. Они также пользуются преимуществами псевдонимов автомонтирования , когда используется пакет фреймворка .
Если вы пишете или содержите библиотеку, которая делает HTTP-запросы, вы можете отделить ее от любой конкретной реализации HTTP-клиента, кодируя в соответствии с Контрактами Symfony (рекомендовано), PSR-18 или HTTPlug v2.
Контракты Symfony
Интерфейсы, которые находятся в пакете symfony/http-client-contracts
,
определяют главные абстракции, реализованные компонентом. Точкой входа
является HttpClientInterface.
Это тот интерфейс, в соответствии с которым вам надо писать код, когда
необходим клиент:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Contracts\HttpClient\HttpClientInterface;
class MyApiLayer
{
public function __construct(
private HttpClientInterface $client,
) {
}
// [...]
}
Все опции запроса, упомянутые выше (например, управление таймаутом), также определяются в текстовом пояснении интерфейса, поэтому все соответствующие реализации (вроде данного компонента) гарантированно их предоставляют. Это существенное отличие от других абстракций, которые не предоставляют ничего, связанного с самим транспортом.
Другая значимая функция, предоставленная Контрактами Symfony, - асинхронность/ мультиплексирование, что было описано в предыдущих разделах.
PSR-18 и PSR-17
Данный компонент реализует спецификации PSR-18 (HTTP-клиент) через класс
Psr18Client, который является адаптером,
превращающим Symfony HttpClientInterface
в PSR-18 ClientInterface
. Этот класс также реализует соответствующие методы PSR-17,
чтобы облегчить создание объектов запроса.
Чтобы использовать его, вам нужен пакет psr/http-client
и реализация PSR-17:
1 2 3 4 5 6 7 8 9 10
# устанавливает PSR-18 ClientInterface
$ composer require psr/http-client
# устанавливает действенную реализацию ответа и фабрики потоков
# c псевдонимами автомонтирования, предоставленными Symfony Flex
$ composer require nyholm/psr7
# как вариант, установите пакет php-http/discovery, чтобы автоматически обнаруживать
# любые уже установленные реализации от общих поставщиков:
# composer require php-http/discovery
Теперь вы можете делать HTTP-запросы с клиентом PSR-18 следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
use Psr\Http\Client\ClientInterface;
class Symfony
{
public function __construct(
private ClientInterface $client,
) {
}
public function getAvailableVersions(): array
{
$request = $this->client->createRequest('GET', 'https://symfony.com/versions.json');
$response = $this->client->sendRequest($request);
return json_decode($response->getBody()->getContents(), true);
}
}
Вы также можете передать набор опций по умолчанию вашему клиенту, благодаря
методу Psr18Client::withOptions()
:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Symfony\Component\HttpClient\Psr18Client;
$client = (new Psr18Client())
->withOptions([
'base_uri' => 'https://symfony.com',
'headers' => [
'Accept' => 'application/json',
],
]);
$request = $client->createRequest('GET', '/versions.json');
// ...
HTTPlug
Спецификация HTTPlug v1 была опубликована до PSR-18 и была вытеснена ею.
Таким образом, вам не стоит использовать ее в свеженаписанном коде. Компонент
все еще взаимосовместим с библиотеками, которые ее требуют, благодаря классу
HttplugClient. Схоже с
Psr18Client, реализующим части PSR-17,
HttplugClient также реализует
методы фабрики, определенные в связанном пакете php-http/message-factory
.
1 2 3 4 5 6 7 8 9
# Давайте представим, что php-http/httplug уже требуется библиотеке, которую вы хотите использовать
# устанавливает эффективную реализацию ответа и фабрики потоков
# с псевдонимами автомонтирования, предоставленными Symfony Flex
$ composer require nyholm/psr7
# как вариант, установите пакет php-http/discovery, чтобы автоматически обнаруживать
# любые уже установленные реализации от общих поставщиков:
# composer require php-http/discovery
Предположим, что вы хотите инстанциировать класс со следующим конструктором, который требует зависимостей HTTPlug:
1 2 3 4 5 6 7 8 9 10 11
use Http\Client\HttpClient;
use Http\Message\StreamFactory;
class SomeSdk
{
public function __construct(
HttpClient $httpClient,
StreamFactory $streamFactory
)
// [...]
}
Так как HttplugClient реализует три интерфейса, вы можете использовать его так:
1 2 3 4
use Symfony\Component\HttpClient\HttplugClient;
$httpClient = new HttplugClient();
$apiClient = new SomeSdk($httpClient, $httpClient);
Если вы хотите работать с обещаниями, HttplugClient
также реализует интерфейс HttpAsyncClient
. Чтобы использовать его, вам нужно установить пакет
guzzlehttp/promises
:
1
$ composer require guzzlehttp/promises
У вас всё готово:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpClient\HttplugClient;
$httpClient = new HttplugClient();
$request = $httpClient->createRequest('GET', 'https://my.api.com/');
$promise = $httpClient->sendAsyncRequest($request)
->then(
function (ResponseInterface $response): ResponseInterface {
echo 'Got status '.$response->getStatusCode();
return $response;
},
function (\Throwable $exception): never {
echo 'Error: '.$exception->getMessage();
throw $exception;
}
);
// когда вы закончите с отправкой нескольких запросов,
// вы должны подождать, чтобы они закончились параллельно
// подождите разрешения конкретного обещания, мониторя все
$response = $promise->wait();
// подождите максимум 1 секунду для разрешения повисших обещаний
$httpClient->wait(1.0);
// подождите разрешения всех оставшихся обещаний
$httpClient->wait();
Вы также можете передать набор опций по умолчанию вашему клиенту, благодаря
методу HttplugClient::withOptions()
:
1 2 3 4 5 6 7 8 9 10
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpClient\HttplugClient;
$httpClient = (new HttplugClient())
->withOptions([
'base_uri' => 'https://my.api.com',
]);
$request = $httpClient->createRequest('GET', '/');
// ...
Нативные PHP-потоки
Ответы, реализующие ResponseInterface, могут быть образованы в нативные PHP-потоки с помощью createResource(). Это позволяет использовать их там, где необходимы нативные PHP-потоки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\StreamWrapper;
$client = HttpClient::create();
$response = $client->request('GET', 'https://symfony.com/versions.json');
$streamResource = StreamWrapper::createResource($response, $client);
// в качестве альтернативы и противоположности предыдущему, это возвращает
// источник, по которому можно проводить поиск и потенциально можно сделать stream_select()
$streamResource = $response->toStream();
echo stream_get_contents($streamResource); // outputs the content of the response
// далее, если вам понадобится, вы можете получить доступ к ответу из потока
$response = stream_get_meta_data($streamResource)['wrapper_data']->getResponse();
Расширяемость
Если вы хотите расширить поведение базового HTTP-клиента, вы можете использовать декорирование сервисов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
class MyExtendedHttpClient implements HttpClientInterface
{
public function __construct(
private HttpClientInterface $decoratedClient = null
) {
$this->decoratedClient ??= HttpClient::create();
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
// обработайте и/или измените $method, $url и/или $options, как вам необходимо
$response = $this->decoratedClient->request($method, $url, $options);
// если здесь вы вызовете любой метод в $response, HTTP-запрос не будет
// асинхронным; см. ниже, чтобы увидеть способ лучше
return $response;
}
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->decoratedClient->stream($responses, $timeout);
}
}
Декоратор вроде этого полезен в случаях, когда обработки аргументов запросов
недостаточно. Декорировав опцию on_progress
, вы можете даже реализовать
базовый мониторинг ответа. Однако, так как вызов методов ответов форсирует
синхронные операции, сделав это внутри request()
, вы нарушите асинхронность.
Решением будет также декорировать сам объект ответа. TraceableHttpClient и TraceableResponse являются хорошими примерами для начала.
Для того, чтобы помочь с написанием более продвинутых процессоров ответов, компонент предоставляет AsyncDecoratorTrait. Эта черта позволяет обрабатывать поток кусков по мере их возвращения из сети:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
class MyExtendedHttpClient implements HttpClientInterface
{
use AsyncDecoratorTrait;
public function request(string $method, string $url, array $options = []): ResponseInterface
{
// обработать и/или изменить $method, $url и/или $options как необходимо
$passthru = function (ChunkInterface $chunk, AsyncContext $context) {
// сделайте с кусками, что хотите, например, разделите
// их на меньшие, сгруппируйте, пропустите некоторые и т.д.
yield $chunk;
};
return new AsyncResponse($this->client, $method, $url, $options, $passthru);
}
}
Так как черта уже реализует конструктор и метод stream()
, вам не нужно
их добавлять. Метод request()
все еще должен быть определен; он должен
возвращать AsyncResponse.
Пользовательская обработка кусков должна происходить в $passthru
: этот
генератор - это то, где вам нужно писать вашу логику. Он будет вызыван для
каждого куска, созданного подлежащим клиентом. $passthru
, который ничего
не делает, просто создаст $chunk;
. Вы также можете создать измененный
кусок, разделить кусок на множество, создав их несколько раз, или даже
пропустить кусок в целом, выпустив return;
вместо создания.
Для того, чтобы контролировать поток, транзит куска получает AsyncContext в качестве второго аргумента. Этот объект контекста имеет методы для чтения текущего состояния ответа. Он также позволяет изменять поток ответа методами для создания новых кусков содержания, паузы потока, отмены потока, изменения информации ответа, замены текущего запроса на другой или изменения самого транзита куска.
Проверка примеров тестирования, реализованных в AsyncDecoratorTraitTest, может быть хорошей точкой начала для получения множества рабочих примеров для лучшего понимания. Вот некоторые примеры использования, которые он симулирует:
- повторная попытка неудачного запроса;
- отправка предполетного запроса, например, для нужд аутентификации;
- выпуск субзапросов и добавление их содержания в тело основного ответа.
Логика в AsyncResponse
имеет много проверок безопасности, которые вызывают LogicException
, если
транзит куска ведет себя некорректно; например, если кусок создается после
isLast()
, или если содержания кусока создается до isFirst()
, и т.д.
Тестирование
Этот компонент включает в себя классы MockHttpClient и MockResponse для использования в тестах, которые не должны делать настоящие HTTP-запросы. Такие тесты могут быть полезны, так как они будут выполняться быстрее и производить стойкие результаты, так как они не зависят от внешнего сервиса. Так как настоящих HTTP-запросов нет, нет необходимости беспокоиться о том, чтобы сервис был онлайн или об изменениях из-за запроса, вроде удаления источника.
MockHttpClient реализует HttpClientInterface, так как и любой настоящий HTTP-клиент в данном компоненте. Когда вы введете HttpClientInterface, ваш код примет реального клиента вне тестов, заменяя его на MockHttpClient в тесте.
Когда метод request
используется в MockHttpClient,
он ответит с помощью предоставленного MockResponse.
Есть несколько способов его использования, как описано ниже.
HTTP-клиент и ответы
Первый способ использования MockHttpClient - передать список ответов его конструктору. Это будет предоставлено в своем порядке при совершении запросов:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$responses = [
new MockResponse($body1, $info1),
new MockResponse($body2, $info2),
];
$client = new MockHttpClient($responses);
// ответы возвращаются в том же порядке, что переданы в MockHttpClient
$response1 = $client->request('...'); // returns $responses[0]
$response2 = $client->request('...'); // returns $responses[1]
Также можно создать
MockResponse непосредственно
из файла, что особенно полезно при хранении скриншотов ответов в файлах:
1 2 3
use Symfony\Component\HttpClient\Response\MockResponse;
$response = MockResponse::fromFile('tests/fixtures/response.xml');
7.1
Метод fromFile() был представлен в Symfony 7.1.
Другой способ использования MockHttpClient заключается в том, чтобы передать обратный вызов, который генерирует ответы динамически, когда его вызывают:
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$callback = function ($method, $url, $options): MockResponse {
return new MockResponse('...');
};
$client = new MockHttpClient($callback);
$response = $client->request('...'); // calls $callback to get the response
Вы также можете передать список обратных вызовов, если вам нужно выполнить определенные утверждения в запросе перед возвратом имитированного ответа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$expectedRequests = [
function ($method, $url, $options): MockResponse {
$this->assertSame('GET', $method);
$this->assertSame('https://example.com/api/v1/customer', $url);
return new MockResponse('...');
},
function ($method, $url, $options): MockResponse {
$this->assertSame('POST', $method);
$this->assertSame('https://example.com/api/v1/customer/1/products', $url);
return new MockResponse('...');
},
];
$client = new MockHttpClient($expectedRequests);
// ...
Tip
Вместо первого аргумента можно также задать (список)
ответов или обратных вызовов с помощью метода
setResponseFactory():
1 2 3 4 5 6 7
$responses = [
new MockResponse($body1, $info1),
new MockResponse($body2, $info2),
];
$client = new MockHttpClient();
$client->setResponseFactory($responses);
Если вам нужно протестировать ответы со статус-кодами HTTP, отличными от 200,
определите опцию http_code
:
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$client = new MockHttpClient([
new MockResponse('...', ['http_code' => 500]),
new MockResponse('...', ['http_code' => 404]),
]);
$response = $client->request('...');
Ответы, предоставленные клиенту-симулятору, не должны быть экземплярами
MockResponse. Любой класс,
реализующий ResponseInterface, будет работать
(например, $this->createMock(ResponseInterface::class)
).
Однако, использование MockResponse позволяет симулирование ответов в кусках и таймаутов:
1 2 3 4 5 6 7 8
$body = function () {
yield 'hello';
// пустые строки превращаются в таймауты, чтобы их легко было тестировать
yield '';
yield 'world';
};
$mockResponse = new MockResponse($body());
Наконец, вы также можете создать вызываемый или итерабельный класс, который генерирует ответы, и использовать его в качестве обратного вызова в функциональных тестах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
namespace App\Tests;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\ResponseInterface;
class MockClientCallback
{
public function __invoke(string $method, string $url, array $options = []): ResponseInterface
{
// загрузите файл набора тестов или сгенерируйте данные
// ...
return new MockResponse($data);
}
}
Затем, сконфигурируйте Symfony для использования вашего обратного вызова:
1 2 3 4 5 6 7 8 9
# config/services_test.yaml
services:
# ...
App\Tests\MockClientCallback: ~
# config/packages/test/framework.yaml
framework:
http_client:
mock_response_factory: App\Tests\MockClientCallback
Чтобы вернуть json, вы обычно будете делать:
1 2 3 4 5 6 7 8 9
use Symfony\Component\HttpClient\Response\MockResponse;
$response = new MockResponse(json_encode([
'foo' => 'bar',
]), [
'response_headers' => [
'content-type' => 'application/json',
],
]);
Вместо этого вы можете использовать JsonMockResponse:
1 2 3 4 5
use Symfony\Component\HttpClient\Response\JsonMockResponse;
$response = new JsonMockResponse([
'foo' => 'bar',
]);
Подобно MockResponse, вы можете
также создать JsonMockResponse
прямо из файла:
1 2 3
use Symfony\Component\HttpClient\Response\JsonMockResponse;
$response = JsonMockResponse::fromFile('tests/fixtures/response.json');
7.1
Метод fromFile() был представлен в Symfony 7.1.
Тестирование данных запроса
Класс MockResponse
поставляется с некоторыми хелпер-методами для
тестирования запроса:
getRequestMethod()
- возвращает HTTP-метод;getRequestUrl()
- возвращает URL, по которому будет отправлен запрос;getRequestOptions()
- возвращает массив, содержащий другую информацию о запросе, вроде заголовков, параметров запроса, содержания тела и т.д.
Пример использования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$mockResponse = new MockResponse('', ['http_code' => 204]);
$httpClient = new MockHttpClient($mockResponse, 'https://example.com');
$response = $httpClient->request('DELETE', 'api/article/1337', [
'headers' => [
'Accept: */*',
'Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l',
],
]);
$mockResponse->getRequestMethod();
// возвращает "DELETE"
$mockResponse->getRequestUrl();
// возвращает "https://example.com/api/article/1337"
$mockResponse->getRequestOptions()['headers'];
// возвращает ["Accept: */*", "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l"]
Полный пример
Следующий отдельный пример демонстрирует способ использования HTTP-клиента и его тестирования в реальном приложении:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
// ExternalArticleService.php
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class ExternalArticleService
{
public function __construct(
private HttpClientInterface $httpClient,
) {
}
public function createArticle(array $requestData): array
{
$requestJson = json_encode($requestData, JSON_THROW_ON_ERROR);
$response = $this->httpClient->request('POST', 'api/article', [
'headers' => [
'Content-Type: application/json',
'Accept: application/json',
],
'body' => $requestJson,
]);
if (201 !== $response->getStatusCode()) {
throw new Exception('Response status code is different than expected.');
}
// ... другие проверки
$responseJson = $response->getContent();
$responseData = json_decode($responseJson, true, 512, JSON_THROW_ON_ERROR);
return $responseData;
}
}
// ExternalArticleServiceTest.php
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class ExternalArticleServiceTest extends TestCase
{
public function testSubmitData(): void
{
// Организовать
$requestData = ['title' => 'Testing with Symfony HTTP Client'];
$expectedRequestData = json_encode($requestData, JSON_THROW_ON_ERROR);
$expectedResponseData = ['id' => 12345];
$mockResponseJson = json_encode($expectedResponseData, JSON_THROW_ON_ERROR);
$mockResponse = new MockResponse($mockResponseJson, [
'http_code' => 201,
'response_headers' => ['Content-Type: application/json'],
]);
$httpClient = new MockHttpClient($mockResponse, 'https://example.com');
$service = new ExternalArticleService($httpClient);
// Действовать
$responseData = $service->createArticle($requestData);
// Утверждать
self::assertSame('POST', $mockResponse->getRequestMethod());
self::assertSame('https://example.com/api/article', $mockResponse->getRequestUrl());
self::assertContains(
'Content-Type: application/json',
$mockResponse->getRequestOptions()['headers']
);
self::assertSame($expectedRequestData, $mockResponse->getRequestOptions()['body']);
self::assertSame($responseData, $expectedResponseData);
}
}
Тестирование с использованием HAR-файлов
Современные браузеры (через их сетевую вкладку) и HTTP-клиенты позволяют экспортировать
информацию одного или нескольких HTTP-запросов, используя формат HAR (HTTP Archive).
Вы можете использовать эти файлы .har
для проведения тестов с помощью HTTP-клиента Symfony.
Сначала с помощью браузера или HTTP-клиента выполните HTTP-запрос(ы), который(ые) вы хотите
протестировать. Затем сохраните эту информацию в виде файла .har
где-нибудь в вашем приложении:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// ExternalArticleServiceTest.php
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class ExternalArticleServiceTest extends TestCase
{
public function testSubmitData(): void
{
// Организовать
$fixtureDir = sprintf('%s/tests/fixtures/HTTP', static::getContainer()->getParameter('kernel.project_dir'));
$factory = new HarFileResponseFactory("$fixtureDir/example.com_archive.har");
$httpClient = new MockHttpClient($factory, 'https://example.com');
$service = new ExternalArticleService($httpClient);
// Действовать
$responseData = $service->createArticle($requestData);
// Утверждать
self::assertSame($responseData, 'the expected response');
}
}
Если ваш сервис выполняет несколько запросов или если ваш файл .har
содержит несколько
пар запрос/ответ, то HarFileResponseFactory
найдет соответствующий ответ на основе метода запроса, URL и тела (если оно есть).
Обратите внимание, что это не будет работать, если тело запроса или URI произвольные / постоянно
меняются (например, если он содержит текущую дату или рандомные UUID).
Тестирование исключений сети транспорта
Как объяснялось в разделе Ошибки сети , при создании HTTP-запросов вы можете столкнуться с ошибками на уровне транспорта.
Поэтому полезно тестировать, как ведёт себя ваше приложение в случае ошибки транспорта. MockResponse позволяет вам делать это, получая исключение из его тела:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// ExternalArticleServiceTest.php
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class ExternalArticleServiceTest extends TestCase
{
// ...
public function testTransportLevelError(): void
{
$requestData = ['title' => 'Testing with Symfony HTTP Client'];
$httpClient = new MockHttpClient([
// Вы можете создать исключения прямо в теле...
new MockResponse([new \RuntimeException('Error at transport level')]),
// ... или вы можете получить исключение из обратного вызова
new MockResponse((static function (): \Generator {
yield new TransportException('Error at transport level');
})()),
]);
$service = new ExternalArticleService($httpClient);
try {
$service->createArticle($requestData);
// Исключение должно было быть вызвано в `createArticle()`, поэтому эта строка не должна быть достигнута
$this->fail();
} catch (TransportException $e) {
$this->assertEquals(new \RuntimeException('Error at transport level'), $e->getPrevious());
$this->assertSame('Error at transport level', $e->getMessage());
}
}
}