@@ -132,9 +132,31 @@ Response::ok(['ok' => true], $cacheControl, ContentType::applicationJson())
132132 ->withHeader(name: 'X-Trace-Id', value: 'abc-123');
133133```
134134
135+ ` withStatus($code, $reasonPhrase) ` honors the supplied reason phrase: when a non-empty string is
136+ passed, ` getReasonPhrase() ` returns it instead of the enum-derived phrase.
137+
138+ ``` php
139+ <?php
140+
141+ declare(strict_types=1);
142+
143+ use TinyBlocks\Http\Code;
144+ use TinyBlocks\Http\Server\Response;
145+
146+ $response = Response::ok(body: null)->withStatus(Code::OK->value, 'All Good');
147+ $response->getReasonPhrase(); # "All Good"
148+ ```
149+
135150#### Setting cookies
136151
137- ` Cookie ` implements ` Headerable ` and composes naturally with ` Response ` :
152+ ` Cookie ` implements ` Headerable ` and composes naturally with ` Response ` .
153+
154+ ` withSameSite(SameSite::NONE) ` automatically enables the ` Secure ` flag. Browsers reject
155+ ` SameSite=None ` cookies that lack it. Calling ` secure() ` separately is not required.
156+
157+ ` withMaxAge(...) ` and ` withExpires(...) ` are mutually exclusive (last-write-wins): setting one
158+ clears the other. This follows RFC 6265 §4.1.2.2, which specifies that ` Max-Age ` takes precedence
159+ over ` Expires ` when both are present.
138160
139161``` php
140162<?php
@@ -155,7 +177,27 @@ $session = Cookie::create(name: 'session', value: $token)
155177Response::ok(['ok' => true], $session);
156178```
157179
180+ Setting ` SameSite=None ` without calling ` secure() ` first is safe. Secure is set automatically:
181+
182+ ``` php
183+ <?php
184+
185+ declare(strict_types=1);
186+
187+ use TinyBlocks\Http\Cookie;
188+ use TinyBlocks\Http\SameSite;
189+ use TinyBlocks\Http\Server\Response;
190+
191+ # Secure is applied automatically when SameSite=None is set.
192+ $crossSite = Cookie::create(name: 'session', value: $token)
193+ ->withSameSite(sameSite: SameSite::NONE);
194+
195+ Response::ok(['ok' => true], $crossSite);
196+ ```
197+
158198To expire a cookie, use ` Cookie::expire(...) ` with the same ` Path ` and ` Domain ` used at creation.
199+ The expired cookie carries both ` Max-Age=0 ` and ` Expires ` set to the Unix epoch: modern browsers
200+ honor ` Max-Age ` . The ` Expires ` fallback covers legacy user agents.
159201
160202``` php
161203<?php
@@ -186,10 +228,14 @@ declare(strict_types=1);
186228
187229use TinyBlocks\Http\Code;
188230
189- Code::OK->value; # 200
190- Code::OK->message(); # "OK"
191- Code::OK->isSuccess(); # true
192- Code::INTERNAL_SERVER_ERROR->isError(); # true
231+ Code::OK->value; # 200
232+ Code::OK->message(); # "OK"
233+ Code::OK->isSuccess(); # true
234+ Code::CONTINUE->isInformational(); # true
235+ Code::MOVED_PERMANENTLY->isRedirection(); # true
236+ Code::BAD_REQUEST->isClientError(); # true
237+ Code::INTERNAL_SERVER_ERROR->isError(); # true
238+ Code::INTERNAL_SERVER_ERROR->isServerError(); # true
193239
194240Code::isValidCode(code: 200); # true
195241Code::isErrorCode(code: 500); # true
@@ -233,22 +279,22 @@ use GuzzleHttp\Psr7\HttpFactory;
233279use TinyBlocks\Http\Client\Transports\NetworkTransport;
234280use TinyBlocks\Http\Http;
235281
282+ $client = new Client(config: ['timeout' => 30, 'connect_timeout' => 5]);
236283$factory = new HttpFactory();
237284
238285$http = Http::with(
239286 baseUrl: 'https://api.example.com',
240287 transport: NetworkTransport::with(
241- client: new Client(config: ['timeout' => 30, 'connect_timeout' => 5]) ,
288+ client: $client ,
242289 factory: $factory
243290 )
244291);
245292```
246293
247294#### Making a request
248295
249- Every parameter on ` Request::create(...) ` is explicit. Pass ` null ` for ` body ` and ` query ` when absent. Pass
250- ` Method::GET ` (or another method) for ` method ` . Build ` headers ` from one or more ` Headerable ` instances via
251- ` Headers::from(...) ` , or call ` Headers::from() ` with no arguments when no headers apply.
296+ Six shortcut factories cover the most common HTTP methods. Supply only the arguments the request
297+ needs. The ` body ` , ` queryParameters ` , and ` headers ` all default to absent or empty.
252298
253299``` php
254300<?php
@@ -258,41 +304,51 @@ declare(strict_types=1);
258304use TinyBlocks\Http\Client\Request;
259305use TinyBlocks\Http\ContentType;
260306use TinyBlocks\Http\Headers;
261- use TinyBlocks\Http\Method;
307+
308+ $response = $http->send(request: Request::get(url: '/v1/charges/abc123'));
262309
263310$response = $http->send(
264- request: Request::create (
311+ request: Request::post (
265312 url: '/v1/charges',
266313 body: ['amount' => 1000, 'currency' => 'usd'],
267- query: null,
268- method: Method::POST,
269314 headers: Headers::from(ContentType::applicationJson())
270315 )
271316);
317+
318+ $response = $http->send(request: Request::delete(url: '/v1/charges/abc123'));
272319```
273320
274- A simple ` GET ` still passes every parameter, with ` Headers::from() ` for the empty header set:
321+ For HTTP methods not covered by the six shortcuts (` OPTIONS ` , ` TRACE ` , ` CONNECT ` , or any custom
322+ method), use ` Request::for(...) ` , which accepts an explicit ` Method ` argument:
275323
276324``` php
277325<?php
278326
279327declare(strict_types=1);
280328
281329use TinyBlocks\Http\Client\Request;
282- use TinyBlocks\Http\Headers;
283330use TinyBlocks\Http\Method;
284331
285332$response = $http->send(
286- request: Request::create(
287- url: '/v1/charges/abc123',
288- body: null,
289- query: null,
290- method: Method::GET,
291- headers: Headers::from()
292- )
333+ request: Request::for(url: '/v1/charges', method: Method::OPTIONS)
293334);
294335```
295336
337+ ` Method ` also exposes RFC 9110 safety and idempotency predicates:
338+
339+ ``` php
340+ <?php
341+
342+ declare(strict_types=1);
343+
344+ use TinyBlocks\Http\Method;
345+
346+ Method::GET->isSafe(); # true (RFC 9110 §9.2.1)
347+ Method::POST->isSafe(); # false
348+ Method::PUT->isIdempotent(); # true (RFC 9110 §9.2.2)
349+ Method::POST->isIdempotent(); # false
350+ ```
351+
296352#### Reading the response
297353
298354``` php
@@ -319,28 +375,29 @@ $hasTrace = $response->headers()->has(name: 'X-Trace-Id'); # true
319375
320376#### Query parameters
321377
322- Pass the query as a named parameter - the library encodes it in RFC3986 form.
378+ Pass query parameters via ` queryParameters: ` . The library encodes them in RFC 3986 form.
323379
324380``` php
325381<?php
326382
327383declare(strict_types=1);
328384
329385use TinyBlocks\Http\Client\Request;
330- use TinyBlocks\Http\Headers;
331- use TinyBlocks\Http\Method;
332386
333387$response = $http->send(
334- request: Request::create (
388+ request: Request::get (
335389 url: '/v1/charges',
336- body: null,
337- query: ['status' => 'succeeded', 'limit' => 50],
338- method: Method::GET,
339- headers: Headers::from()
390+ queryParameters: ['status' => 'succeeded', 'limit' => 50]
340391 )
341392);
342393```
343394
395+ To replace query parameters on an existing request, use ` withQueryParameters(...) ` :
396+
397+ ``` php
398+ $updated = $request->withQueryParameters(queryParameters: ['limit' => 100]);
399+ ```
400+
344401#### Custom headers and content type
345402
346403Compose any combination of ` Headerable ` via ` Headers::from(...) ` :
@@ -354,7 +411,6 @@ use TinyBlocks\Http\Client\Request;
354411use TinyBlocks\Http\ContentType;
355412use TinyBlocks\Http\Headerable;
356413use TinyBlocks\Http\Headers;
357- use TinyBlocks\Http\Method;
358414
359415final readonly class IdempotencyKey implements Headerable
360416{
@@ -369,11 +425,9 @@ final readonly class IdempotencyKey implements Headerable
369425}
370426
371427$response = $http->send(
372- request: Request::create (
428+ request: Request::post (
373429 url: '/v1/charges',
374430 body: ['amount' => 1000],
375- query: null,
376- method: Method::POST,
377431 headers: Headers::from(
378432 ContentType::applicationJson(),
379433 new IdempotencyKey(value: $key)
@@ -384,12 +438,25 @@ $response = $http->send(
384438
385439Custom headers always win over the library's JSON defaults.
386440
441+ To add or replace a single header on an existing request, use ` withHeader(...) ` . The lookup is
442+ case-insensitive: replacing ` Content-Type ` via ` content-type ` still finds and replaces the entry.
443+
444+ ``` php
445+ <?php
446+
447+ declare(strict_types=1);
448+
449+ use TinyBlocks\Http\Client\Request;
450+
451+ $updated = Request::get(url: '/v1/charges')
452+ ->withHeader(name: 'X-Trace-Id', value: 'abc-123');
453+ ```
454+
387455#### Setting the User-Agent
388456
389457The ` UserAgent ` value object implements ` Headerable ` and renders the standard
390- ` User-Agent ` header. Empty version is normalized to "no version" - the rendered
391- header carries only the product token in that case, so configuration with an
392- optional version flows in directly.
458+ ` User-Agent ` header. An absent or empty version is normalized to "no version". The rendered
459+ header carries only the product token in that case.
393460
394461``` php
395462<?php
@@ -398,17 +465,13 @@ declare(strict_types=1);
398465
399466use TinyBlocks\Http\Client\Request;
400467use TinyBlocks\Http\Headers;
401- use TinyBlocks\Http\Method;
402468use TinyBlocks\Http\UserAgent;
403469
404470$userAgent = UserAgent::from(product: 'MyApp', version: '1.2.3');
405471
406472$response = $http->send(
407- request: Request::create (
473+ request: Request::get (
408474 url: '/v1/charges',
409- body: null,
410- query: null,
411- method: Method::GET,
412475 headers: Headers::from($userAgent)
413476 )
414477);
@@ -437,15 +500,12 @@ declare(strict_types=1);
437500use TinyBlocks\Http\Client\Request;
438501use TinyBlocks\Http\ContentType;
439502use TinyBlocks\Http\Headers;
440- use TinyBlocks\Http\Method;
441503use TinyBlocks\Http\UserAgent;
442504
443505$response = $http->send(
444- request: Request::create (
506+ request: Request::post (
445507 url: '/v1/charges',
446508 body: ['amount' => 1000],
447- query: null,
448- method: Method::POST,
449509 headers: Headers::from(
450510 UserAgent::from(product: 'MyApp', version: '1.2.3'),
451511 ContentType::applicationJson()
0 commit comments