phasync
HTTP client / HttpClient

Class

HttpClient

A PSR-18 HTTP client that runs concurrent requests on curl_multi.

Introduction#

sendRequest() follows PSR-18: it returns once the response headers have arrived, or throws a Psr\Http\Client\ClientExceptionInterface: RequestException for an invalid request, NetworkException when no response arrives because of a network failure, and ClientException for any other failure. Inside a coroutine only the calling coroutine is suspended while it waits; outside one the call blocks until the response has arrived. The body keeps streaming after it returns, and a failure while reading it is thrown from the stream read. An HTTP error status such as 404 or 500 is a normal response and throws nothing.

sendAsyncRequest(), get(), post(), put() and request() are not PSR-18: they return a CurlResponse at once, before the transfer has started, so that several requests can run together. The transfer starts when the response's status, headers or body is first read, every request created up to then starts together, and a failure is thrown by that first read.

Cancelling a coroutine that is waiting for a response throws phasync\CancelledException in it. The client stays usable.

Class synopsis#

final class HttpClient implements ClientInterface {
    /* Methods */
    public HttpClient::__construct(HttpClientOptions|array $defaultRequestOptions = [])
    public HttpClient::addMiddlewareFunction(Closure $middleware): void
    public HttpClient::sendRequest(RequestInterface $request): ResponseInterface
    public HttpClient::sendAsyncRequest(RequestInterface $request): ResponseInterface
    public HttpClient::request(
        string $method,
        UriInterface|string $url,
        mixed $requestData = null,
        HttpClientOptions|array|null $options = null
    ): ResponseInterface
    public HttpClient::get(
        UriInterface|string $url,
        HttpClientOptions|array|null $options = null
    ): ResponseInterface
    public HttpClient::post(
        UriInterface|string $url,
        mixed $requestData,
        HttpClientOptions|array|null $options = null
    ): ResponseInterface
    public HttpClient::put(
        UriInterface|string $url,
        mixed $requestData,
        HttpClientOptions|array|null $options = null
    ): ResponseInterface
}

Methods#

MethodDescription
__constructCreate a client whose options are the defaults for every request it sends.
addMiddlewareFunctionWrap HttpClient::sendRequest in a function that can change the request or the response.
sendRequestSend a PSR-7 request with the client's default options.
sendAsyncRequestSend a PSR-7 request without waiting: the lazy counterpart of HttpClient::sendRequest.
requestSend a request with any method, a body and options for this request only.
getSend a GET request.
postSend a POST request with a body.
putSend a PUT request with a body.

Examples#

Example 1Two requests that run together
use phasync\HttpClient\HttpClient;

$base   = require __DIR__ . '/../server.php';   // a local server for the example
$client = new HttpClient(['timeoutMs' => 5000]);

// Neither transfer has started yet: both start together at the first read
$slow = $client->get("$base/slow?seconds=0.2&name=slow");
$fast = $client->get("$base/slow?seconds=0.1&name=fast");

echo $slow->getStatusCode(), ' ', $slow->getBody(), "\n";
echo $fast->getStatusCode(), ' ', $fast->getBody(), "\n";
Output
200 slow
200 fast
Example 2Requests in coroutines
use phasync\HttpClient\HttpClient;

$base   = require __DIR__ . '/../server.php';
$client = new HttpClient();

// Inside phasync::run() a request suspends only the coroutine that waits for it
phasync::run(function () use ($client, $base) {
    foreach (['b' => 0.2, 'a' => 0.1] as $name => $seconds) {
        phasync::go(function () use ($client, $base, $name, $seconds) {
            $body = (string) $client->get("$base/slow?seconds=$seconds&name=$name")->getBody();
            echo "response $body\n";
        });
    }
    echo "both requests sent\n";
});
Output
both requests sent
response a
response b
Example 3
use phasync\HttpClient\HttpClient;

$client = new HttpClient(['timeoutMs' => 5000]);

// Neither transfer has started yet, so both run at the same time once the first is read.
$a = $client->get('https://example.com/a');
$b = $client->get('https://example.com/b');

echo $a->getStatusCode(), "\n";
echo $b->getBody();

// PSR-18: returns when the headers have arrived, or throws.
try {
    $response = $client->sendRequest(\phasync\Psr\Request::create('GET', 'https://example.com/c'));
} catch (\Psr\Http\Client\NetworkExceptionInterface $e) {
    echo 'no response: ', $e->getMessage();
}

See also#

  • HttpClient::requestSend a request with any method, a body and options for this request only.
  • HttpClientOptionsThe options of an HTTP request, as public properties that map to cURL options.
  • CurlResponseThe PSR-7 response of an HttpClient request, filled in by a cURL transfer.
  • NetworkExceptionNo response arrived, or the response stopped arriving, because of a network failure.
  • RequestExceptionThe request could not be sent because it is invalid.
  • ClientExceptionA request failed for a reason that is neither an invalid request nor a network failure.
  • MultipartStreamA multipart/form-data request body built from a nested array of fields.
  • phasync::goStarts $fn as a coroutine in the current frame and scope, and returns it, without waiting for it.