Http — Request, Transport, Response

Drie heldere lagen: Request beschrijft de call, Transport voert hem uit, Response komt terug. Cache is een decorator (CachingTransport), niet ingebakken in de Request.

Url — fluent en immutable

parse() leest een url; withQuery() / withPath() leveren een nieuwe instance.

$url  = Url::parse('https://api.example.com/v1/items?page=1');
$next = $url->withQuery(['page' => 2, 'limit' => 50]);

return '<code>' . htmlspecialchars((string) $next) . '</code>';
https://api.example.com/v1/items?page=2&limit=50

Request — with-pattern

Elke setter levert een clone. De originele $base blijft hergebruikbaar.

$base = Request::get('https://api.example.com/users')
    ->withHeader('Accept', 'application/json')
    ->withTimeout(5.0);

$page2 = $base->withQuery(['page' => 2])->withCacheTtl(300);

return [
    'method'  => $page2->method->value,
    'url'     => (string) $page2->url,
    'ttl'     => $page2->cacheTtl,
    'headers' => $page2->headers->toLines(),
];
Array
(
    [method] => GET
    [url] => https://api.example.com/users?page=2
    [ttl] => 300
    [headers] => Array
        (
            [0] => Accept: application/json
        )

)

Client met FakeTransport

Voor testen / pages zonder netwerk: implementeer Transport zelf.

$fake = new class implements TransportInterface {
    public function send(Request $r): Response {
        return new Response(200, new Headers(['Content-Type' => 'application/json']),
            '{"users":[{"id":1,"name":"Alice"}]}');
    }
};

$client = new Client($fake);
$resp   = $client->get('https://api.example.com/users');

return $resp->json(associative: true);
Array
(
    [users] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [name] => Alice
                )

        )

)

CachingTransport — TTL via Request

Tweede call met dezelfde TTL gaat niet meer naar de inner transport. ($fake en $cacheDir komen uit de page-setup.)

$caching = new CachingTransport($fake, $cacheDir);
$req     = Request::get('https://api.example.com/users')->withCacheTtl(60);

$before = count($fake->received);
$caching->send($req);   // → fake.send()
$caching->send($req);   // → uit cache, geen tweede fake-call
$after = count($fake->received);

return ['fake_received' => $after - $before, 'verwacht' => 1];
Array
(
    [fake_received] => 1
    [verwacht] => 1
)

cacheKey: stabiel, exclusief auth-headers

Authorization en Cookie-headers tellen niet mee — dezelfde resource met andere user levert dezelfde cache-key.

$a = Request::get('https://example.com/x')->withHeader('Authorization', 'Bearer USER-A');
$b = Request::get('https://example.com/x')->withHeader('Authorization', 'Bearer USER-B');

return ['gelijk?' => $a->cacheKey() === $b->cacheKey()];
Array
(
    [gelijk?] => 1
)

Response — raw blijft, json() is opt-in

In tegenstelling tot library/Http: de raw body blijft beschikbaar, json() throwt nooit (returnt null bij parse-fout).

$r = new Response(200, new Headers(['Content-Type' => 'application/json']), '{"ok":true}');

return [
    'status'   => $r->status,
    'isOk'     => $r->isOk(),
    'json.ok'  => $r->json()->ok,
    'raw_body' => $r->body,
];
Array
(
    [status] => 200
    [isOk] => 1
    [json.ok] => 1
    [raw_body] => {"ok":true}
)

Voeg ?live=1 toe aan de URL om een echte HTTP-call te zien (httpbin.org).