From 165b90c5eeaffc8693d3b6523b39689170e8ad55 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:09:02 +0400 Subject: [PATCH 1/2] docs: document the Logger message type, the rate limiter folders and the core:env lookup --- v3.0/packages/console/built-in-commands.md | 1 + v3.0/packages/logger/contracts.md | 2 ++ v3.0/packages/ratelimit/adapters.md | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/v3.0/packages/console/built-in-commands.md b/v3.0/packages/console/built-in-commands.md index 35bdaea..3c8a663 100644 --- a/v3.0/packages/console/built-in-commands.md +++ b/v3.0/packages/console/built-in-commands.md @@ -89,6 +89,7 @@ Contract: - the command requires `.env.example` in the project base directory - if `.env` already exists, the command asks before overwriting unless `--yes` is passed +- both files are looked up in the project base directory, not in the folder you run the command from, so the question appears wherever you start it ### `core:key` diff --git a/v3.0/packages/logger/contracts.md b/v3.0/packages/logger/contracts.md index 2ba0f41..bc8a4d1 100644 --- a/v3.0/packages/logger/contracts.md +++ b/v3.0/packages/logger/contracts.md @@ -16,6 +16,8 @@ The Logger package is small, but a few runtime contracts matter when you build o - `debug()` - `log()` +Each method takes the message as `string|Stringable $message` and an optional `array $context`; `log()` takes the level first. A class that extends `Logger` and overrides these methods must declare the same parameter type. + But the package does not validate arbitrary custom level strings in `log()`. Instead: diff --git a/v3.0/packages/ratelimit/adapters.md b/v3.0/packages/ratelimit/adapters.md index 3def06e..bbc64aa 100644 --- a/v3.0/packages/ratelimit/adapters.md +++ b/v3.0/packages/ratelimit/adapters.md @@ -43,7 +43,7 @@ Use it when the app runs on one machine or when each node can keep its own indep ### What it guarantees -- The storage directory is created if it does not exist. +- The storage directory is created if it does not exist, together with its missing parent folders (for example `cache/data` when `cache/` is missing). - Each hit stores `count` and `reset_at`. - When the window expires, the next hit starts a fresh counter. - `retryAfter()` returns `0` when the state file is missing or unreadable. From 69197ec454a60bee47ba11a261f3dffee2df13f6 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:13:34 +0400 Subject: [PATCH 2/2] docs: correct the HttpClient errors and document the request body and response decoding --- v3.0/packages/httpclient/architecture.md | 4 +-- v3.0/packages/httpclient/overview.md | 13 +++++++-- v3.0/packages/httpclient/usage.md | 35 ++++++++++++++++++++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/v3.0/packages/httpclient/architecture.md b/v3.0/packages/httpclient/architecture.md index 44173ad..df42e4f 100644 --- a/v3.0/packages/httpclient/architecture.md +++ b/v3.0/packages/httpclient/architecture.md @@ -47,7 +47,7 @@ For a normal multi request, `start()` calls the native `MultiCurlAdapter`. Each ### Async multi request path -For async multi requests, Quantum installs the same completion collector used by normal multi requests and also wires your success and error callbacks. Your callbacks are useful for per-request side effects, while `getResponse()` and `getErrors()` remain available after `start()` finishes. +For async multi requests, Quantum installs the same completion collector used by normal multi requests and also wires your success and error callbacks. For each finished request the collector runs first, so inside your callback `getResponse()` and `getErrors()` already hold that request. Your callbacks are useful for per-request side effects, while `getResponse()` and `getErrors()` remain available after `start()` finishes. ## Response storage model @@ -78,7 +78,7 @@ The package keeps a separate `$requestHeaders` array for headers you set through - `setHeader($name, $value)` - `setHeaders([...])` -Those keys are lowercased before storage. +Those keys are lowercased before storage, so read them back in lowercase too: after `setHeader('X-Token', 'abc')`, `getRequestHeaders('x-token')` returns `abc` and `getRequestHeaders('X-Token')` returns `null`. `setHeader()` adds one entry, while `setHeaders()` replaces the whole tracked set. This is only a bookkeeping layer for later reads through `getRequestHeaders()`. It does not inspect headers set through unrelated low-level cURL options. diff --git a/v3.0/packages/httpclient/overview.md b/v3.0/packages/httpclient/overview.md index 37a1e60..ff82223 100644 --- a/v3.0/packages/httpclient/overview.md +++ b/v3.0/packages/httpclient/overview.md @@ -12,7 +12,7 @@ This package is intentionally small. It does not add retries, middleware, authen ## Package shape -The package is built from three pieces: +The package is built from these pieces: - `Quantum\HttpClient\HttpClient` is the runtime wrapper - `Quantum\HttpClient\Factories\HttpClientFactory` creates fresh request facades @@ -61,7 +61,9 @@ Use this when your application wants callback handling while still allowing late - `PATCH` - `DELETE` -Any other method triggers `HttpClientException::requestMethodNotAvailable(...)`. +Any other method triggers `HttpClientException::requestMethodNotAvailable(...)`. The list is also available as `HttpClient::METHODS`. + +You can read back what you set with `getMethod()` and `getData()`, get the underlying adapter with `getAdapter()`, and ask `isMultiRequest()` which kind of request the client holds. ## Response model @@ -73,6 +75,13 @@ Responses are stored in three sections: Headers are normalized to lowercase before storage, so `Content-Type` becomes `content-type` when you read it back. +The `body` is decoded for you: + +- when the `Content-Type` contains `json` and the body is valid JSON, it is the result of `json_decode()`, so an object (`stdClass`), not an array +- when the `Content-Type` contains `xml` and the body is valid XML, it is a `SimpleXMLElement` +- a body sent with `Content-Encoding: gzip` is decompressed first +- in every other case, including invalid JSON or XML, it is the raw string + For a single request, `getResponse()` returns one response array. For multi requests, `getResponse()` returns a map keyed by curl request ID. diff --git a/v3.0/packages/httpclient/usage.md b/v3.0/packages/httpclient/usage.md index 8f65934..d9b9468 100644 --- a/v3.0/packages/httpclient/usage.md +++ b/v3.0/packages/httpclient/usage.md @@ -21,6 +21,8 @@ Because response headers are normalized to lowercase, read them like this: $contentType = $client->getResponseHeaders('content-type'); ``` +A JSON or XML response body arrives already decoded; see the [response model](overview.md#response-model). + ## Send JSON or form data in a single request ```php @@ -31,9 +33,21 @@ $client = httpRequest('https://api.example.com/users') ->start(); ``` -`setData()` is passed through the native adapter's post-data builder before the request is executed. +How `setData()` is sent depends on the `Content-Type` header you set before `start()`: + +- an array (or a `JsonSerializable`) with an `application/json` or `application/...+json` header is sent as a JSON body +- an array without a JSON header is form-encoded, like `name=Arman&role=admin` +- an array with a `multipart/form-data` header, or one that holds a `CURLFile`, is sent as multipart, which is how you upload a file +- a string is sent as it is + +```php +$client = httpRequest('https://api.example.com/avatar') + ->setMethod('POST') + ->setData(['user' => 'arman', 'avatar' => new CURLFile('/path/to/avatar.png')]) + ->start(); +``` -## Check transport errors +## Check errors ```php $errors = $client->getErrors(); @@ -44,22 +58,31 @@ if (!empty($errors)) { } ``` -The package records transport-layer curl errors. It does not treat HTTP 4xx or 5xx responses as package exceptions by itself. +`getErrors()` is empty when the request worked. It holds an entry when cURL failed to complete the request, and also when the server answered with an HTTP 4xx or 5xx status. A request to a missing page gives: + +```php +['code' => 404, 'message' => 'HTTP/1.1 404 Not Found'] +``` + +For a cURL failure, `code` is the cURL error number and `message` is its description. Neither case throws an exception. The response body of a 4xx or 5xx answer is still available through `getResponseBody()`. ## Queue multiple requests ```php $client = httpMultiRequest(); -$client->addGet('https://api.example.com/users'); +$client->addGet('https://api.example.com/users', ['page' => 2]); $client->addGet('https://api.example.com/teams'); +$client->addPost('https://api.example.com/teams', ['name' => 'Core']); $client->start(); $responses = $client->getResponse(); $errors = $client->getErrors(); ``` -In multi mode, `$responses` is keyed by curl request ID. Inspect each entry's `headers`, `cookies`, and `body` sections. +`addGet()` appends its data array to the URL as a query string, with `&` when the URL already has one, so the first request above asks for `/users?page=2`. `addPost()` builds its body the same way as `setData()`. + +In multi mode, `$responses` is keyed by curl request ID. Inspect each entry's `headers`, `cookies`, and `body` sections. `$errors` is keyed the same way and holds only the requests that failed. ## Use async callbacks for multi requests @@ -79,7 +102,7 @@ $client->addGet('https://api.example.com/users'); $client->start(); ``` -In this mode, handle each result inside your callbacks when you need per-request side effects. `getResponse()` and `getErrors()` are also populated after `start()` finishes. +In this mode, handle each result inside your callbacks when you need per-request side effects. A request that failed, including one that got an HTTP 4xx or 5xx answer, goes to the error callback; every other request goes to the success callback. `getResponse()` and `getErrors()` are also populated after `start()` finishes. ## Recommended usage patterns