Guzzle PHP: 12 сценариев + настоящий локальный HTTP-стенд
=======================================================

Материал для ae-nekrasov.ru. Требования: PHP 8.2+ в рамках ветки 8.x,
Composer 2. Для live.php также нужен ext-curl. Зависимости устанавливаются
из включённого composer.lock. vendor в ZIP не включён.

1. БЫСТРЫЙ СТАРТ (ВНЕШНИЕ API НЕ НУЖНЫ)

Откройте терминал в распакованной папке и выполните:

    php --version
    composer install
    composer demo -- all

Установка пакетов требует доступа к их репозиториям. После установки
demo.php полностью автономен: все ответы и сетевые ошибки подставляет
MockHandler. Адрес api.example.test вымышленный, запросы к нему не уходят.

Команды отдельных сценариев:

    composer demo -- list
    composer demo -- get
    composer demo -- json
    composer demo -- form
    composer demo -- headers
    composer demo -- multipart
    composer demo -- 404
    composer demo -- 503
    composer demo -- network
    composer demo -- invalid-json
    composer demo -- invalid-schema
    composer demo -- download
    composer demo -- async

Прямой эквивалент без Composer scripts: php demo.php get

2. ЧТО ПРОВЕРЯЕТСЯ

get            query, кодирование кириллицы и пробелов, путь base_uri, JSON.
json           POST, JSON-тело, Content-Type, ответ HTTP201.
form           application/x-www-form-urlencoded и массив полей.
headers        Bearer и Basic; только вымышленные реквизиты, вывод скрыт.
multipart      Текстовое поле, имя/байты файла, автоматический boundary.
404            ClientException и ручная проверка при http_errors=false.
503            ServerException, Retry-After; автоматического повтора нет.
network        ConnectException даже при http_errors=false.
invalid-json   HTTP200 с неверным JSON вызывает JsonException при разборе.
invalid-schema JSON корректен, но структура id/name не подходит приложению.
download       sink пишет во временный файл, содержимое проверяется.
async          Utils::settle собирает успешный ответ, HTTP503 и сетевой сбой.

Middleware::history фиксирует сформированные запросы. Самопроверки
сравнивают метод, URI, заголовки, содержимое и ожидаемые исключения.
Надпись PASS означает, что поведение соответствует сценарию. Ожидаемая
ошибка (например, HTTP404) успешно перехвачена и поэтому тоже даёт PASS.
Код выхода: 0 — все проверки пройдены; 1 — проверка не прошла;
2 — неизвестное имя сценария или отсутствует curl для live.php.

MockHandler НЕ проверяет настоящий DNS, TLS, длительность timeout,
параллельность транспорта и ускорение запросов. network имитирует timeout,
а async демонстрирует состояния promises и обработку частичных ошибок.
Проверка id/name — ручная проверка структуры, не реализация JSON Schema.
History хранится в памяти; не включайте неограниченный сбор production-трафика.

3. НАСТОЯЩИЕ ЗАПРОСЫ НА LOCALHOST

В первом терминале из папки проекта:

    php -S 127.0.0.1:8095 local-server.php

Во втором терминале из той же папки:

    composer request
    composer live
    composer compare

request.php выводит: 7: Александр
curl-vs-guzzle.php отправляет один и тот же GET /users/7?lang=ru&include=profile
двумя способами: расширением PHP cURL и Guzzle. Оба используют Accept,
X-Request-ID, таймауты 1/2с, проверку HTTP2xx, JSON и структуры id/name.
В cURL curl_exec=false/errno означает сбой передачи; при FAILONERROR=false
HTTP404 сам по себе не делает curl_exec=false. Код ответа проверяется отдельно.
В Guzzle для равного сравнения задан http_errors=false; обычное значение true
превращает HTTP4xx/5xx в исключения. Guzzle сам может использовать cURL как
транспорт: это высокоуровневый API, а не замена HTTP-протокола или JSON-проверки.
Его практическое удобство — общий Client, default headers, query/json/form_params/
multipart, PSR-7 и middleware; поддержка некоторых опций зависит от handler.
live.php проверяет 10 условий через настоящий cURL и HTTP:
идентичность сервера, GET пользователя, GET query, POST JSON, POST формы,
multipart, sink, исключение HTTP404, HTTP503 без исключения, реальный timeout.
Ожидается: Все 10 проверок localhost пройдены.
Остановите сервер сочетанием Ctrl+C в первом терминале.
При занятом порте сначала освободите именно свой процесс или выберите другой
порт одновременно в команде сервера, request.php и live.php.

Маршруты local-server.php:

GET /health          {ok:true, fixture:"ae-nekrasov-guzzle-demo"}
GET /users/7         {id:7, name:"Александр"}
GET/POST /echo       {method, query, headers, json, form}
POST /upload         {file:{name,size,error}, form}; поле файла: file
GET /file            Текст "Guzzle download demo\n"
GET /status/404      HTTP404 + {error:"Not found"}
GET /status/503      HTTP503 + Retry-After: 1
GET /slow            Ответ через 400мс; timeout в live.php — 100мс

В /echo поля query/headers/form — JSON-объекты; json=null, если JSON-тела нет.
Файл из multipart только проверяется в upload_tmp_dir PHP; приложение его
никуда не переносит. sink использует tmpfile(), файл удаляется при fclose().
Локальный клиент отключает системный proxy и перенаправления. На реальном
API настройте proxy, HTTPS, авторизацию и разрешённые URL под свою задачу.

PHP built-in server предназначен для разработки. Привязывайте его только
к 127.0.0.1 и не публикуйте в интернете. Здесь он последовательный: эта
установка не подходит для измерения конкурентности или производительности.
Пример проверяет общий timeout cURL, но не отдельный connect_timeout,
TLS-сертификаты, DNS, отказы внешнего провайдера или production-нагрузку.

4. КАК ПЕРЕНЕСТИ ПРИМЕР В ПРОЕКТ

require __DIR__ . '/vendor/autoload.php' подключает загрузчик Composer.
Создайте Client с base_uri, разумным timeout и Accept, затем используйте
get/post/request. У base_uri с путём завершайте путь слешем. Относительный
users сохранит этот путь; /users начнёт путь от корня домена.

Для обычного JSON используйте json, для HTML-форм — form_params,
для файла — multipart. Выберите один формат тела: не сочетайте эти опции
с body или друг с другом. В multipart не задавайте Content-Type вручную:
Guzzle сам согласует boundary и тело.

Токены берите из переменной окружения или менеджера секретов. Не копируйте
вымышленные реквизиты как рабочие и не выводите Authorization/Cookie в лог.
Обрабатывайте HTTP, транспорт и ошибки данных отдельно. ConnectException
не наследуется от RequestException; общий интерфейс — GuzzleException.
http_errors=false влияет на HTTP4xx/5xx, но не отключает сетевые исключения.
200 и правильный Content-Type сами по себе не гарантируют валидный JSON
или подходящую структуру. JSON_THROW_ON_ERROR позволяет заметить сбой.

timeout ограничивает общую передачу. connect_timeout поддерживает встроенный
cURL handler; без него поведение зависит от обработчика. read_timeout
относится к отдельному чтению потока при stream=true, это другой лимит.
TLS verify оставляйте включённым на HTTPS. Для проблем CA исправляйте
настройку доверенных сертификатов, не применяйте verify=false как решение.
Повторы Guzzle сам не делает: учитывайте идемпотентность, лимит попыток,
общий бюджет времени и Retry-After. POST повторяйте только с пониманием
контракта API и механизма предотвращения дубликатов.

5. КОМАНДЫ COMPOSER

    composer validate --strict
    composer check-platform-reqs
    composer audit --locked
    composer show guzzlehttp/guzzle

composer install использует версии composer.lock. Обновляйте зависимости
осознанно через composer update, затем запускайте проверки заново.
Руководство по Composer:
https://ae-nekrasov.ru/development/composer-php-rukovodstvo-komandy-primery/

6. ИСТОЧНИКИ

https://docs.guzzlephp.org/en/stable/quickstart.html
https://docs.guzzlephp.org/en/stable/request-options.html
https://docs.guzzlephp.org/en/stable/testing.html
https://docs.guzzlephp.org/en/stable/handlers-and-middleware.html
https://github.com/guzzle/promises
https://www.php.net/manual/en/function.json-decode.php
https://www.php.net/manual/en/function.curl-exec.php
https://www.php.net/manual/en/function.curl-getinfo.php

Сам учебный код распространяется по MIT (LICENSE). Установленные сторонние
пакеты имеют собственные лицензии, доступные в vendor после composer install.
