HTTP-расширение PHP: от базовых запросов до асинхронного пула
Расширение HTTP для PHP: отправка и обработка HTTP-запросов
Расширение ext/http (также известное как pecl_http) предоставляет набор классов для удобной работы с HTTP-протоколом в PHP. С его помощью можно отправлять HTTP-запросы, обрабатывать ответы, управлять заголовками, куки, создавать собственные HTTP-серверы. Расширение активно используется для интеграции с внешними API, краулинга, тестирования и разработки микросервисов.
Основным и наиболее эффективным подходом в современных версиях PHP (7.4+) является использование классов Http\Client и Http\Message, которые поддерживают PSR-7 и PSR-18 стандарты. Рассмотрим типичный сценарий отправки GET-запроса и обработки ответа.
<?php
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
// Автоматическое обнаружение клиента (может быть Guzzle, cURL и т.д.)
$httpClient = HttpClientDiscovery::find();
$requestFactory = MessageFactoryDiscovery::find();
// Создание запроса
$request = $requestFactory->createRequest('GET', 'https://api.example.com/data');
// Отправка и получение ответа
$response = $httpClient->sendRequest($request);
// Обработка ответа
$statusCode = $response->getStatusCode();
$headers = $response->getHeaders();
$body = (string) $response->getBody();
echo "Статус: " . $statusCode . "\n";
echo "Тело ответа: " . substr($body, 0, 200);
?>
Docker php ext (расширение php в docker)
В этом примере используется библиотека php-http/discovery, которая автоматически находит установленный HTTP-клиент (например, Guzzle или Symfony HttpClient). Расширение ext/http предоставляет собственную реализацию Http\Client\HttpClient, поэтому данный код будет работать с любым PSR-18 совместимым клиентом.
Типичные ошибки:
- Если не установлены зависимости (например, через Composer не подтянут пакет php-http/discovery), возникнет исключение Http\Discovery\NotFoundException. Решение: выполнить composer require php-http/discovery php-http/httplug.
- Для работы с реальным расширением ext-http необходимо установить само расширение через PECL или менеджер пакетов ОС. В Ubuntu: sudo apt install php-http. В macOS: pecl install pecl_http.
Как выполнить HTTP-запрос с помощью старого класса HttpRequest?
В более ранних версиях расширения (до 3.x) основным классом был HttpRequest. Он все еще доступен, но рекомендуется переходить на современный PSR-7 API.
<?php
$request = new HttpRequest('https://api.example.com/data', HttpRequest::METH_GET);
$request->setOptions(['timeout' => 10]);
$request->addHeaders(['Accept' => 'application/json']);
try {
$response = $request->send();
echo $response->getBody();
echo $response->getResponseCode();
} catch (HttpException $e) {
echo 'Ошибка: ' . $e->getMessage();
}
?>
Ext http php (расширение http для php)
Проблемы: класс HttpRequest не реализует PSR-7, зависит от глобальных настроек. При больших нагрузках может вызывать утечки памяти (не освобождает ресурсы cURL).
Типичная ошибка:
Если расширение установлено, но класс не найден, возможно, используется PHP 8+ и требуется версия pecl_http 4.x, где старые классы были удалены. В таком случае следует применять только PSR-18 API.
Как отправить POST-запрос с данными формы через HttpClient?
Для POST-запроса требуется установить метод и тело запроса. В PSR-7 тело - это поток (StreamInterface).
<?php
use Http\Message\StreamFactory\GuzzleStreamFactory;
use Http\Message\RequestFactory\GuzzleRequestFactory;
use Http\Client\HttpClient;
use Http\Discovery\HttpClientDiscovery;
$client = HttpClientDiscovery::find();
$requestFactory = new GuzzleRequestFactory();
$streamFactory = new GuzzleStreamFactory();
$body = $streamFactory->createStream(http_build_query(['name' => 'John', 'age' => 30]));
$request = $requestFactory->createRequest(
'POST',
'https://httpbin.org/post',
['Content-Type' => 'application/x-www-form-urlencoded'],
$body
);
$response = $client->sendRequest($request);
echo $response->getBody();
?>
Off php ext (отключение расширения php)
Здесь использованы фабрики из пакета php-http/guzzle6-adapter. Если установлен только чистый ext-http, потребуется написать свои фабрики или использовать его встроенную реализацию PSR-18.
Проблемы при работе с телом потока:
Нельзя повторно читать поток после первого считывания без перемотки. Необходимо вызывать $stream->rewind() или сохранять содержимое в строку через (string) $stream.
Как удобно собрать строку запроса с помощью HttpQueryString?
Класс HttpQueryString позволяет формировать и разбирать строки запроса (query string).
<?php
$query = new HttpQueryString();
$query->set(['page' => 2, 'limit' => 20, 'sort' => 'name']);
echo $query->toString(); // ?page=2&limit=20&sort=name
// Разбор существующей строки
$query2 = new HttpQueryString('?user=123&token=abc');
print_r($query2->toArray());
?>
Ext php install (установка расширения php)
Этот класс упрощает конструирование URL, избегая ручной конкатенации и экранирования.
Ошибка:
Если строка запроса уже содержит знак вопроса, HttpQueryString его не добавляет автоматически - результат может начинаться с ? или без него. Требуется проверять формат.
Как управлять HTTP-заголовками с помощью HttpHeader?
Класс HttpHeader предоставляет методы для парсинга и формирования заголовков.
<?php
$header = new HttpHeader('Content-Type', 'application/json; charset=utf-8');
echo $header->toString(); // Content-Type: application/json; charset=utf-8
// Парсинг существующего заголовка
$parsed = HttpHeader::fromString('Authorization: Bearer abc123');
echo $parsed->getName(); // Authorization
echo $parsed->getValue(); // Bearer abc123
?>
Полезно для проверки входящих заголовков при создании собственного HTTP-сервера.
Как создать простой HTTP-сервер на PHP с помощью HttpServer?
Расширение включает класс HttpServer, позволяющий запустить встроенный сервер без использования внешних библиотек.
<?php
class MyHandler extends HttpRequestHandler {
public function handle(HttpRequest $request) {
$response = new HttpResponse();
$response->setContent('Hello from PHP HTTP server!');
return $response;
}
}
$server = new HttpServer();
$server->setOption('host', '127.0.0.1');
$server->setOption('port', 8080);
$server->setHandler(new MyHandler());
$server->start();
?>
Этот сервер работает синхронно, обрабатывая запросы по очереди. Для продакшена лучше использовать Nginx или Apache с PHP-FPM, но для тестов и прототипов это удобно.
Проблемы:
- Сервер может не ответить на запросы, если не вызван setHandler().
- При ошибках в обработчике сервер может зависнуть. Нужна обработка исключений.
- На Windows класс HttpServer может не поддерживаться (зависит от сборки расширения).
Как использовать HttpRequestPool для параллельных запросов?
HttpRequestPool позволяет выполнять несколько запросов асинхронно (в одном потоке).
<?php
$pool = new HttpRequestPool();
$requests = [
new HttpRequest('http://example.com/1'),
new HttpRequest('http://example.com/2'),
new HttpRequest('http://example.com/3'),
];
foreach ($requests as $req) {
$pool->attach($req);
}
$pool->send();
foreach ($pool->getFinished() as $req) {
echo $req->getResponseBody() . PHP_EOL;
}
?>
Этот подход ускоряет выполнение, когда нужно обратиться к нескольким ресурсам. Однако пул блокирует текущий процесс до завершения всех запросов.
Ошибки:
Если один из запросов падает с ошибкой (таймаут), send() может выбросить исключение. Рекомендуется оборачивать в try-catch или настроить опции таймаута.
Расширенные примеры работы с HTTP расширением PHP
Пример 1: Отправка PUT-запроса с JSON и получение подробной информации об ответе
<?php
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
$httpClient = HttpClientDiscovery::find();
$requestFactory = MessageFactoryDiscovery::find();
$streamFactory = StreamFactoryDiscovery::find();
$data = ['title' => 'Тест', 'content' => 'Пример обновления'];
$body = $streamFactory->createStream(json_encode($data, JSON_UNESCAPED_UNICODE));
$request = $requestFactory->createRequest(
'PUT',
'https://jsonplaceholder.typicode.com/posts/1',
['Content-Type' => 'application/json', 'Accept' => 'application/json'],
$body
);
$response = $httpClient->sendRequest($request);
echo "Статус: " . $response->getStatusCode() . "\n";
echo "Заголовки:\n";
foreach ($response->getHeaders() as $name => $values) {
echo " $name: " . implode(', ', $values) . "\n";
}
$responseBody = (string) $response->getBody();
echo "Тело ответа:\n$responseBody\n";
?>
Статус: 200
Заголовки:
date: Fri, 07 Jul 2023 12:00:00 GMT
content-type: application/json; charset=utf-8
...
Тело ответа:
{
"userId": 1,
"id": 1,
"title": "Тест",
"content": "Пример обновления",
"completed": false
}
Пояснение: в примере создается поток из JSON-строки, отправляется PUT-запрос, затем выводится полная информация об ответе. Важно указывать заголовок Content-Type, чтобы сервер правильно интерпретировал тело.
Пример 2: Работа с куки через HttpCookie
<?php
// Создание куки
$cookie = new HttpCookie('session_id', 'abc123');
$cookie->setDomain('.example.com');
$cookie->setPath('/');
$cookie->setMaxAge(3600); // 1 час
echo $cookie->toString() . "\n";
// Парсинг куки из строки
$parsed = HttpCookie::fromString('user=ivan; path=/; expires=Wed, 21 Oct 2025 07:28:00 GMT');
echo $parsed->getName() . ": " . $parsed->getValue() . "\n";
// Использование в запросе
$request = new HttpRequest('https://example.com/profile', HttpRequest::METH_GET);
$request->setCookies([$cookie, $parsed]);
$response = $request->send();
?>
session_id=abc123; domain=.example.com; path=/; max-age=3600 user: ivan
Класс HttpCookie упрощает создание и разбор куки, а также их прикрепление к запросу. Обратите внимание на корректное указание домена и пути для избежания проблем с безопасностью.
Пример 3: Обработка больших ответов с потоковым чтением
<?php
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
$httpClient = HttpClientDiscovery::find();
$requestFactory = MessageFactoryDiscovery::find();
$request = $requestFactory->createRequest('GET', 'https://speed.hetzner.de/100MB.bin');
$response = $httpClient->sendRequest($request);
$bodyStream = $response->getBody();
// Поочередное чтение частями по 8192 байта
while (!$bodyStream->eof()) {
$chunk = $bodyStream->read(8192);
// Обработка чанка, например, запись в файл
file_put_contents('downloaded_part', $chunk, FILE_APPEND);
}
echo "Файл загружен частями.\n";
?>
Пояснение: для работы с большими файлами не следует загружать весь ответ в память - используется потоковый режим. Метод read() возвращает указанное количество байт, eof() проверяет конец потока. В данном примере файл сохраняется порциями.
Пример 4: Создание и отправка multipart/form-data запроса
<?php
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
$httpClient = HttpClientDiscovery::find();
$requestFactory = MessageFactoryDiscovery::find();
$streamFactory = StreamFactoryDiscovery::find();
// Формирование multipart-тела вручную (упрощенно)
$boundary = '----WebKitFormBoundary' . uniqid();
$bodyParts = [
"--$boundary\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\nIvan",
"--$boundary\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"photo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n" . file_get_contents('photo.jpg'),
"--$boundary--\r\n"
];
$body = $streamFactory->createStream(implode("\r\n", $bodyParts));
$request = $requestFactory->createRequest(
'POST',
'https://httpbin.org/post',
['Content-Type' => "multipart/form-data; boundary=$boundary"],
$body
);
$response = $httpClient->sendRequest($request);
echo $response->getBody();
?>
Этот пример демонстрирует ручное составление multipart-тела. В реальных проектах рекомендуется использовать специализированные библиотеки (например, php-http/multipart-stream-builder), так как ручное формирование чревато ошибками в границах и кодировке.
Типичная ошибка:
Если не указать символы \r\n в правильной последовательности, сервер может не распознать части запроса. Строго соблюдать спецификацию RFC 2046.
Пример 5: Асинхронные запросы через HttpRequestPool с обработкой ошибок
<?php
$pool = new HttpRequestPool();
$urls = [
'https://httpstat.us/200',
'https://httpstat.us/404',
'https://httpstat.us/500',
];
$requests = [];
foreach ($urls as $url) {
$req = new HttpRequest($url, HttpRequest::METH_GET);
$req->setOptions(['timeout' => 5]);
$requests[] = $req;
$pool->attach($req);
}
try {
$pool->send();
} catch (HttpException $e) {
echo "Ошибка пула: " . $e->getMessage() . "\n";
}
foreach ($pool->getFinished() as $req) {
$info = $req->getResponseInfo();
$code = $req->getResponseCode();
echo "URL: " . $req->getUrl() . " -> Код: $code\n";
if ($code == 200) {
echo "Тело: " . substr($req->getResponseBody(), 0, 50) . "...\n";
} else {
echo "Ошибка: " . $req->getResponseStatus() . "\n";
}
}
?>
URL: https://httpstat.us/200 -> Код: 200 Тело: 200 OK... URL: https://httpstat.us/404 -> Код: 404 Ошибка: Not Found URL: https://httpstat.us/500 -> Код: 500 Ошибка: Internal Server Error
В этом примере параллельно отправляются три запроса, результаты обрабатываются независимо. Важно установить таймауты, чтобы при зависании одного запроса не блокировать весь пул надолго. Исключение ловится на уровне пула, но каждый запрос может содержать свою ошибку, которую можно проверить через методы getResponseCode() и getResponseStatus().