ZeroPHP

HTTP Client

HTTP Client

Namespace: Zero\Lib\Http (the Http facade), Zero\Lib\Http\PendingRequest, Zero\Lib\Http\ClientResponse.

The HTTP client wraps PHP's cURL extension. There are two ways to use it:

  • Fluent (preferred) — returns a ClientResponse
  • Legacy static (back-compatible) — returns a plain stdClass with ok, status, body, json, error
use Zero\Lib\Http;

// Fluent
$r = Http::timeout(10)->acceptJson()->get('https://api.example.com/posts');
if ($r->successful()) {
    $posts = $r->json('data', []);
}

// Legacy
$r = Http::get($url, $query, $headers, $timeout);
if ($r->ok) {
    $data = $r->json;
}

Requests don't throw by default — call $r->throw() (or ->throw(true) on the pending request) to opt in.


Setting up the request

Implementation: PendingRequest.php. Every fluent setter on Http::* returns a PendingRequest. Terminal verbs (get/post/put/patch/delete/head/send) execute and return a ClientResponse.

Http::timeout(int $seconds): PendingRequest

Total request timeout (also applies to connect when connectTimeout not set).

Http::timeout(5)->get($url);

Http::connectTimeout(int $seconds): PendingRequest

Http::connectTimeout(2)->timeout(10)->get($url);

Http::withHeaders(array $headers): PendingRequest

Http::withHeaders(['X-A' => '1', 'X-B' => '2'])->get($url);

Http::withHeader(string $name, string $value): PendingRequest

Http::withHeader('X-Trace', $traceId)->get($url);

Http::acceptJson(): PendingRequest

Http::acceptJson()->get($url);

Http::accept(string $contentType): PendingRequest

Http::accept('application/xml')->get($url);

Http::asJson(): PendingRequest

JSON-encode the body (default).

Http::asJson()->post($url, ['k' => 'v']);

Http::asForm(): PendingRequest

URL-encoded body.

Http::asForm()->post($url, ['email' => $email]);

Http::asMultipart(): PendingRequest

Multipart body — usually pair with attach().

Http::asMultipart()->attach('file', '/tmp/x.png')->post($url);

Http::bodyFormat(string $format): PendingRequest

Low-level switch: json, form, multipart, or body (raw string passthrough).

Http::bodyFormat('body')->contentType('text/xml')->post($url, $rawXml);

Http::contentType(string $type): PendingRequest

Http::contentType('application/xml')->post($url, $xml);

Http::withToken(string $token, string $type = 'Bearer'): PendingRequest

Http::withToken($accessToken)->get($url);

Http::withBasicAuth(string $username, string $password): PendingRequest

Http::withBasicAuth('user', 'pass')->get($url);

Http::withQueryParameters(array $query): PendingRequest

Merged with any query passed to the verb.

Http::withQueryParameters(['page' => 2])->get($url);

Http::withCookies(array $cookies): PendingRequest

Http::withCookies(['session' => $sid, 'theme' => 'dark'])->get($url);

Http::withUserAgent(string $userAgent): PendingRequest

Http::withUserAgent('Zero/1.0')->get($url);

Http::withoutVerifying(): PendingRequest

Disable TLS peer/host verification (development only).

Http::withoutVerifying()->get('https://self-signed.test');

Http::baseUrl(string $url): PendingRequest

Prefix relative URLs.

Http::baseUrl('https://api.example.com')->get('/users'); // https://api.example.com/users

Http::withOptions(array $options): PendingRequest

Raw curl option overrides.

Http::withOptions([CURLOPT_FOLLOWLOCATION => false])->get($url);

Http::attach(string $name, mixed $contents, ?string $filename = null, array $headers = []): PendingRequest

Multipart upload. $contents may be a path on disk or an in-memory string.

Http::attach('avatar', '/tmp/me.png', 'avatar.png')->post($url);
Http::attach('payload', $bytes, 'payload.bin')->post($url);

Http::retry(int $times, int $sleepMs = 0, ?callable $when = null): PendingRequest

Auto-retry on failure. Optional $when callback receives ($response, $attempt).

Http::retry(3, 200)->get($url);
Http::retry(3, 200, fn ($r, $i) => $r->serverError())->get($url);

Http::dump(): PendingRequest / Http::dd(): PendingRequest

Debug the outgoing request (then continue / die).

Http::dump()->withToken($t)->get($url);

Default headers (process-wide)

Http::setDefaultHeaders(['X-Client' => 'zero/1.0']);
Http::addDefaultHeader('X-Trace', $traceId);

These persist for both legacy and fluent calls in the same process.


Verb methods

All return a ClientResponse when called fluently.

->get(string $url, array $query = []): ClientResponse

Http::acceptJson()->get('https://api.example.com/users', ['page' => 2]);

->head(string $url, array $query = []): ClientResponse

Returns headers only (no body).

Http::acceptJson()->head($url);

->post(string $url, mixed $data = null, array $query = []): ClientResponse

Http::asJson()->post($url, ['email' => $email]);

->put(string $url, mixed $data = null, array $query = []): ClientResponse

Http::asJson()->put("$url/$id", $payload);

->patch(string $url, mixed $data = null, array $query = []): ClientResponse

Http::asJson()->patch("$url/$id", ['status' => 'active']);

->delete(string $url, mixed $data = null, array $query = []): ClientResponse

Http::asJson()->delete("$url/$id");

->send(string $method, string $url, mixed $data = null, array $query = []): ClientResponse

Custom verb (or runtime-chosen).

Http::asJson()->send('PURGE', $url);

Inspecting the response

Implementation: ClientResponse.php.

status(): int

Http::get($url)->status(); // 200

body(): string

$body = Http::get($url)->body();

json(?string $key = null, $default = null): mixed

Decode the JSON body. Dot-notation supported.

$r = Http::asJson()->get($url);
$r->json();                       // full decoded array
$r->json('data.user.id');         // dot-path lookup
$r->json('missing', 'fallback');  // default

object(): ?object

JSON decoded as stdClass.

Http::asJson()->get($url)->object()?->user?->id;

headers(): array / header(string $name): ?string

$r = Http::get($url);
$r->headers();             // ['Content-Type' => ['application/json'], ...]
$r->header('Content-Type'); // 'application/json'

Status checks

$r = Http::get($url);
$r->ok();             // status === 200
$r->successful();     // 200..299
$r->redirect();       // 300..399
$r->clientError();    // 400..499
$r->serverError();    // 500..599
$r->failed();         // connection error OR 4xx OR 5xx
$r->unauthorized();   // 401
$r->forbidden();      // 403
$r->notFound();       // 404

error(): ?string

Underlying curl error, if any.

Http::timeout(1)->get('http://10.255.255.1')->error(); // 'Operation timed out...'

throw(): self

Throw RuntimeException if failed(); otherwise return $this for chaining.

$r = Http::asJson()->get($url)->throw();

__toString()

Casts to the response body.

$body = (string) Http::get($url);

Legacy static API

The original positional-argument API still works and returns a plain stdClass:

$r = Http::get($url, $query, $headers, $timeout);
$r->ok;     // bool
$r->status; // int
$r->body;   // string|null
$r->json;   // decoded array or null
$r->error;  // string|null

Available on Http::get, Http::post, Http::put, Http::patch, Http::delete. Default headers from Http::setDefaultHeaders() apply to both APIs.

On this page

HTTP ClientSetting up the requestHttp::timeout(int $seconds): PendingRequestHttp::connectTimeout(int $seconds): PendingRequestHttp::withHeaders(array $headers): PendingRequestHttp::withHeader(string $name, string $value): PendingRequestHttp::acceptJson(): PendingRequestHttp::accept(string $contentType): PendingRequestHttp::asJson(): PendingRequestHttp::asForm(): PendingRequestHttp::asMultipart(): PendingRequestHttp::bodyFormat(string $format): PendingRequestHttp::contentType(string $type): PendingRequestHttp::withToken(string $token, string $type = 'Bearer'): PendingRequestHttp::withBasicAuth(string $username, string $password): PendingRequestHttp::withQueryParameters(array $query): PendingRequestHttp::withCookies(array $cookies): PendingRequestHttp::withUserAgent(string $userAgent): PendingRequestHttp::withoutVerifying(): PendingRequestHttp::baseUrl(string $url): PendingRequestHttp::withOptions(array $options): PendingRequestHttp::attach(string $name, mixed $contents, ?string $filename = null, array $headers = []): PendingRequestHttp::retry(int $times, int $sleepMs = 0, ?callable $when = null): PendingRequestHttp::dump(): PendingRequest / Http::dd(): PendingRequestDefault headers (process-wide)Verb methods->get(string $url, array $query = []): ClientResponse->head(string $url, array $query = []): ClientResponse->post(string $url, mixed $data = null, array $query = []): ClientResponse->put(string $url, mixed $data = null, array $query = []): ClientResponse->patch(string $url, mixed $data = null, array $query = []): ClientResponse->delete(string $url, mixed $data = null, array $query = []): ClientResponse->send(string $method, string $url, mixed $data = null, array $query = []): ClientResponseInspecting the responsestatus(): intbody(): stringjson(?string $key = null, $default = null): mixedobject(): ?objectheaders(): array / header(string $name): ?stringStatus checkserror(): ?stringthrow(): self__toString()Legacy static API