HTTP-клієнт

Дата оновлення перекладу 2024-06-07

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_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-клієнт, наданий даним компонентом, не фіксує стан, але обробка куки вимагає сховища фіксації стану (так як відповіді можуть оновлювати куки, і вони повинні бути використані для запитів, що випливають). Тому цей компонент не обробляє куки автоматично.

Ви можете або відправляти куки з компонентом 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-сертифікатів (а браузери використовують власні сховища). При використанні самопідписаних сертифікатів під час розробки, рекомендується створювати власний авторитет сертифікатів (СА) та додавати його у сховище вашої системи.

Як варіант, ви також можете віключити 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. Він повернеться назад до AmpHttpClient, якщо cURL не вдалося знайти або він застарілий. Нарешті, якщо 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
$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');

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() ResponseInterface:

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" у фрагменті на це, код стає повністю асинхронним:

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, 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

# встановлює дієву реалізацію відповіді та фабрики потоків
# з псевдонімами автомонтування, наданими 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
12
13
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;

class SomeSdk
{
    public function __construct(
        HttpClient $httpClient,
        RequestFactory $requestFactory,
        StreamFactory $streamFactory
    )
    // [...]
}

Так як HttplugClient реалізує три інтерфейси, ви можете використати його так:

1
2
3
4
use Symfony\Component\HttpClient\HttplugClient;

$httpClient = new HttplugClient();
$apiClient = new SomeSdk($httpClient, $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); // виводить зміст відповіді

// далі, якщо вам знадобиться, ви можете отримати доступ до відповіді з потоку
$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) {
    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-архів). Ви можете використовувати ці файли .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());
        }
    }
}