HTTP-клієнт

Дата оновлення перекладу 2022-12-15

HTTP-клієнт

Установка

Компонент HttpClient - це низькорівневий HTTP-клієнт з підтримкою як обгорток PHP-стрімів, так і cURL. Він надає інструменти для споживання API та підтримує синхронні та асинхронні операції. Ви можете встановити його за допомогою:

1
$ composer require symfony/http-client

Базове використання

Використовуйте клас HttpClient, щоб робити запити. У фреймворку Symfony, цей клас доступний як сервіс http_client. Цей сервіс буде автозмонтований при введенні підказки HttpClientInterface:

  • Framework Use
  • Standalone Use
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 Symfony\Contracts\HttpClient\HttpClientInterface;

class SymfonyDocs
{
    private $client;

    public function __construct(HttpClientInterface $client)
    {
        $this->client = $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:

  • YAML
  • XML
  • PHP
  • Standalone Use
1
2
3
4
5
# config/packages/framework.yaml
framework:
    http_client:
        default_options:
            max_redirects: 7

Ви також можете використати метод withOptions(), щоб вилучити новий екземпляр клієнта з новими опціями за замовчуванням:

1
2
3
4
$this->client = $client->withOptions([
    'base_uri' => 'https://...',
    'headers' => ['header-name' => 'header-value']
]);

Деякі опції, описані у цьому посібнику:

Перегляньте повний довідник конфігурації http_client , щоб дізнатися про всі опції.

HTTP-клієнт також має одну опцію конфігурації під назвою max_host_connections, ця опція не момже бути перевизначена запитом:

  • YAML
  • XML
  • PHP
  • Standalone Use
1
2
3
4
5
# config/packages/framework.yaml
framework:
    http_client:
        max_host_connections: 10
        # ...

Визначення клієнта

Часто буває так, що деякі опції HTTP-клієнта залежать від URL запиту (наприклад, ви повинні встановити деякі заголовки під час запиту до GitHub API, але не до інших хостів). Якщо це ваш випадок, компонент надає визначених клієнтів (використовуючи ScopingHttpClient) для автоконфігурації HTTP-клієнта на основі запитуваного URL:

  • YAML
  • XML
  • PHP
  • Standalone Use
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-клієнт підтримує різноманітні механізми аутентифікації. Вони можуть бути визначені глобально у конфігурації (щоб застосовуватия до всіх запитів) і до кожного запиту окремо (що перевизначає будь-яку глобальну аутентифікацію):

  • YAML
  • XML
  • PHP
  • Standalone Use
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, щоб визначити заголовки, за замовчуванням додані до всіх запитів:

  • YAML
  • XML
  • PHP
  • Standalone Use
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();

Щоб відправити форму із завантаженими файлами, ви повинні зашифрувати тіло відповідно до типу змісту multipart/form-data. Компонент Symfony Mime перетворює це на декілька рядків коду:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;

$formFields = [
    'regular_field' => 'some value',
    'file_field' => DataPart::fromPath('/path/to/uploaded/file'),
];
$formData = new FormDataPart($formFields);
$client->request('POST', 'https://...', [
    'headers' => $formData->getPreparedHeaders()->toArray(),
    'body' => $formData->bodyToIterable(),
]);

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.

Перенаправлення

За замовчуванням, 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, щоб вирішити, чи потрібна запиту повторна спроба, та визначити час очікування між всіма повторними спробами.

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],
]);

Це налаштування не вплине на інших клієнтів.

Продуктивність

Компонент створений для максимальної HTTP-продуктивності. Він сумісний з HTTP/2 та створенням асинхронних потокових та мультиплексних запитів/відповідей, що перетинаються. Навіть при регулярних синхронних викликах, він дозволяє залишати зʼєднання з віддаленими хостами відкритими між запитами, що покращує продуктивність, зберігаючи DNS дозвіл, SSL перемовини і т.д., що повторюються. Щоб користуватися всіма цими перревагами, необхідно розширення cURL.

Підключення підтримки cURL

Цей компонент підтримує як нативні PHP-потоки, так і cURL, щоб робити HTTP-запити. Хоча вони взаємозамінні та надають однакові функції, включно з запитами, що перетинаються, HTTP/2 підтримується лише при використанні cURL.

HttpClient::create() обирає транспорт cURL, якщо увімкнено PHP-розширення cURL, і використовує PHP-потоки - якщо ні. Якщо ви надаєте перевагу чіткому вибору транспорту, використовуйте наступні класи для створення клієнта:

1
2
3
4
5
6
7
8
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;

// використовує нативні PHP-потоки
$client = new NativeHttpClient();

// використовує PHP-розширення cURL
$client = new CurlHttpClient();

При використанні цього компонента у повностековому додатку 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 Compression

The HTTP header Accept-Encoding: gzip is added automatically if:

  • When using cURL client: cURL was compiled with ZLib support (see php --ri curl)
  • When using the native HTTP client: Zlib PHP extension is installed

If the server does respond with a gzipped response, it's decoded transparently. To disable HTTP compression, send an Accept-Encoding: identity HTTP header.

Chunked transfer encoding is enabled automatically if both your PHP runtime and the remote server supports it.

Підтримка HTTP/2

При запиті URL https URL, HTTP/2 вмикається за замовчуванням, якщо встановлено один з наступних інструментів:

  • Пакет libcurl версії 7.36 або вище;
  • Пакет Packagist amphp/http-client версії 4.2 або вище.

Щоб форсувати HTTP/2 для URL http, вам потрібно чітко його включити через опцію http_version:

  • YAML
  • XML
  • PHP
  • Standalone Use
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->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() HTTP-клієнтів приймає список відповідей для моніторингу. Як було загадано раніше , цей метод створює частини відповідей по мірі їх надходження з мережі. Замінивши "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.

Споживання подій, відправлених сервером

Події, відправлені сервером - це інтернет-стандарт для завантаження даних на веб-сторінки. Його 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) {
            // сделайте что-то с событием сервера ...
        }
    }
}

Взаємосумісність

Компонент взаємосумісний з чотирма різними абстракціями для 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
12
13
use Symfony\Contracts\HttpClient\HttpClientInterface;

class MyApiLayer
{
    private $client;

    public function __construct(HttpClientInterface $client)
    {
        $this->client = $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 наступним чином:

  • Framework Use
  • Standalone Use
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Psr\Http\Client\ClientInterface;

class Symfony
{
    private $client;

    public function __construct(ClientInterface $client)
    {
        $this->client = $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);
    }
}

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) {
            echo 'Got status '.$response->getStatusCode();

            return $response;
        },
        function (\Throwable $exception) {
            echo 'Error: '.$exception->getMessage();

            throw $exception;
        }
    );

// коли ви закінчите з відправкою декількох запитів, ви маєте
// зачекати, щоб вони закінчилися паралельно

// почекайтей на розвʼязання конкретної обіцянки, моніторячи всі
$response = $promise->wait();

// зачекайте максимум 1 секунду для розвʼязання обіцянок, що підвисли
$httpClient->wait(1.0);

// зачекайте розвʼязання решти обіцянок
$httpClient->wait();

Нативні 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
25
class MyExtendedHttpClient implements HttpClientInterface
{
    private $decoratedClient;

    public function __construct(HttpClientInterface $decoratedClient = null)
    {
        $this->decoratedClient = $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]

Другий спосіб використовувати 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

Tip

Instead of using the first argument, you can also set the (list of) responses or callbacks using the setResponseFactory() method:

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 для використання вашого зворотного виклику:

  • YAML
  • XML
  • PHP
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

Тестування даних запиту

Клас 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
74
75
// ExternalArticleService.php
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class ExternalArticleService
{
    private HttpClientInterface $httpClient;

    public function __construct(HttpClientInterface $httpClient)
    {
        $this->httpClient = $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);
    }
}