diff --git a/lib/ApiClient.php b/lib/ApiClient.php index 1da793a..0c64da4 100644 --- a/lib/ApiClient.php +++ b/lib/ApiClient.php @@ -45,15 +45,18 @@ class ApiClient extends \GuzzleHttp\Client private RateLimiter $rateLimiter; /** - * {@inheritDoc} + * Initialize the API client with configuration, certificate validation, and a rate limiter. * - * $config['clientid'] - obtained from Developer Portal - when you registered your app with us. - * $config['cert'] = ['/path/to/cert.p12','certificat password'] - * $config['clientpubip'] = the closest IP address to the real end-user - * $config['mocking'] = true to use /rbcz/premium/mock/* endpoints + * Accepted $config keys: + * - 'clientid': client ID from the Developer Portal. + * - 'cert': array with [pathToP12, password]; when omitted, CERT_FILE and CERT_PASS configuration values are used. + * - 'clientpubip': client public IP (nearest to the end user). + * - 'mocking': bool to enable mock endpoints. + * - 'debug': debug flag. * - * @throws \Exception CERT_FILE is not set - * @throws \Exception CERT_PASS is not set + * @param array $config Client configuration. + * @throws \Exception If certificate file path (CERT_FILE) is not provided. + * @throws \Exception If certificate password (CERT_PASS) is not provided. */ public function __construct(array $config = []) { @@ -202,13 +205,11 @@ public static function checkCertificatePassword(string $certFile, string $passwo } /** - * Request Identifier. + * Produce a short request identifier used for diagnostics and testing. * - * @todo Obtain using RateLimiter + * @deprecated since version 0.1 — Do not use in production environments. * - * @deprecated since version 0.1 - Do not use in production Environment! - * - * @return string + * @return string The generated request identifier composed from a source token and the current timestamp, truncated to at most 59 characters. */ public static function getxRequestId() { @@ -216,13 +217,13 @@ public static function getxRequestId() } /** - * Send an HTTP request. - * - * @param array $options Request options to apply to the given - * request and to the transfer. See \GuzzleHttp\RequestOptions. + * Send an HTTP request while enforcing and updating client rate limits. * - * @throws GuzzleException - * @throws RateLimitExceededException + * @param \Psr\Http\Message\RequestInterface $request The HTTP request to send. + * @param array $options Request options to apply to the transfer. See \GuzzleHttp\RequestOptions. + * @return \Psr\Http\Message\ResponseInterface The HTTP response. + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws RateLimitExceededException If the client is rate limited and wait mode is disabled. */ public function send(\Psr\Http\Message\RequestInterface $request, array $options = []): \Psr\Http\Message\ResponseInterface { @@ -253,4 +254,4 @@ public function send(\Psr\Http\Message\RequestInterface $request, array $options return $response; } -} +} \ No newline at end of file diff --git a/lib/RateLimit/JsonRateLimitStore.php b/lib/RateLimit/JsonRateLimitStore.php index 51f6231..3ee197b 100644 --- a/lib/RateLimit/JsonRateLimitStore.php +++ b/lib/RateLimit/JsonRateLimitStore.php @@ -20,6 +20,13 @@ class JsonRateLimitStore implements RateLimitStoreInterface private string $filename; private array $data = []; + /** + * Create a JsonRateLimitStore backed by the given file and load any existing data. + * + * If the file exists, its JSON contents are decoded into the in-memory store; decoding failures result in an empty dataset. + * + * @param string $filename Path to the JSON file used to persist rate limit data. + */ public function __construct(string $filename) { $this->filename = $filename; @@ -30,11 +37,26 @@ public function __construct(string $filename) } } + /** + * Retrieve the stored rate-limit entry for a client and window. + * + * @param string $clientId The client identifier. + * @param string $window The rate-limit window identifier. + * @return array{remaining:int,timestamp:int}|null The entry for the specified client and window: an array with keys `remaining` and `timestamp`, or `null` if not found. + */ public function get(string $clientId, string $window): ?array { return $this->data[$clientId][$window] ?? null; } + /** + * Store the remaining quota and associated timestamp for a client's rate-limit window and persist it to the configured JSON file. + * + * @param string $clientId Identifier of the client. + * @param string $window Identifier of the rate-limit window. + * @param int $remaining Number of remaining requests for the specified window. + * @param int $timestamp Unix timestamp associated with the stored entry. + */ public function set(string $clientId, string $window, int $remaining, int $timestamp): void { $this->data[$clientId][$window] = [ @@ -45,13 +67,24 @@ public function set(string $clientId, string $window, int $remaining, int $times $this->save(); } + /** + * Retrieve all rate-limit window entries for a client. + * + * @param string $clientId The client identifier. + * @return array An associative array of windows to entries where each entry contains keys `remaining` (int) and `timestamp` (int); returns an empty array if the client has no entries. + */ public function allForClient(string $clientId): array { return $this->data[$clientId] ?? []; } + /** + * Persist the in-memory rate limit data to the configured JSON file. + * + * Writes the current $data as pretty-printed JSON into $this->filename. + */ private function save(): void { file_put_contents($this->filename, json_encode($this->data, \JSON_PRETTY_PRINT)); } -} +} \ No newline at end of file diff --git a/lib/RateLimit/PdoRateLimitStore.php b/lib/RateLimit/PdoRateLimitStore.php index 840c8be..cb01715 100644 --- a/lib/RateLimit/PdoRateLimitStore.php +++ b/lib/RateLimit/PdoRateLimitStore.php @@ -19,12 +19,25 @@ class PdoRateLimitStore implements RateLimitStoreInterface { private \PDO $pdo; + /** + * Store the PDO connection and ensure the rate_limits table exists. + * + * Initializes the store by saving the provided PDO instance and creating the + * `rate_limits` table if it does not already exist. + */ public function __construct(\PDO $pdo) { $this->pdo = $pdo; $this->init(); } + /** + * Fetches the remaining token count and expiry timestamp for a client's rate-limit window. + * + * @param string $clientId Identifier of the client. + * @param string $window Identifier of the rate-limit window. + * @return array{remaining:int, timestamp:int}|null The row for the specified client and window with integer `remaining` and `timestamp`, or `null` if no record exists. + */ public function get(string $clientId, string $window): ?array { $stmt = $this->pdo->prepare(<<<'EOD' @@ -40,6 +53,14 @@ public function get(string $clientId, string $window): ?array return $row ?: null; } + /** + * Store or update the rate-limit record for a client and window. + * + * @param string $clientId Identifier of the client. + * @param string $window Identifier of the rate-limit window (e.g., "1m", "hourly"). + * @param int $remaining Number of remaining allowed requests for the window. + * @param int $timestamp UNIX timestamp associated with the record (seconds since epoch). + */ public function set(string $clientId, string $window, int $remaining, int $timestamp): void { $stmt = $this->pdo->prepare(<<<'EOD' @@ -51,6 +72,12 @@ public function set(string $clientId, string $window, int $remaining, int $times $stmt->execute([$clientId, $window, $remaining, $timestamp]); } + /** + * Fetches all rate-limit entries for a client, indexed by window. + * + * @param string $clientId The client identifier. + * @return array Associative array keyed by window name; each value contains `remaining` (int) and `timestamp` (int). + */ public function allForClient(string $clientId): array { $stmt = $this->pdo->prepare(<<<'EOD' @@ -74,6 +101,12 @@ public function allForClient(string $clientId): array return $results; } + /** + * Ensure the `rate_limits` table exists in the connected PDO database. + * + * Creates a table with columns: `client_id` (TEXT), `window` (TEXT), `remaining` (INTEGER), + * `timestamp` (INTEGER) and a composite primary key on (`client_id`, `window`). + */ private function init(): void { $this->pdo->exec(<<<'EOD' @@ -88,4 +121,4 @@ private function init(): void EOD); } -} +} \ No newline at end of file diff --git a/lib/RateLimit/RateLimitStore.php b/lib/RateLimit/RateLimitStore.php index c7d7e1f..c26e068 100644 --- a/lib/RateLimit/RateLimitStore.php +++ b/lib/RateLimit/RateLimitStore.php @@ -19,6 +19,13 @@ abstract class RateLimitStore { protected string $path; + /** + * Initialize the store with the backing file path and ensure the file exists. + * + * If the file at the provided path does not exist, creates it containing an empty JSON array. + * + * @param string $path Filesystem path to the JSON file used to persist rate-limit data. + */ public function __construct(string $path) { $this->path = $path; @@ -28,9 +35,30 @@ public function __construct(string $path) } } - abstract public function get(string $key): array; - abstract public function increment(string $key, string $field, int $ttlSeconds): int; + /** + * Retrieve stored rate-limit entries for the specified key. + * + * @param string $key The identifier of the rate-limit bucket or resource. + * @return array An array of rate-limit records for the key, or an empty array if none exist. + */ +abstract public function get(string $key): array; + /** + * Increment the numeric counter for a rate-limited key and field, applying a time-to-live. + * + * @param string $key The identifier for the rate-limited entity. + * @param string $field The specific counter field to increment. + * @param int $ttlSeconds Time to live for the counter in seconds. + * @return int The counter value after the increment. + */ +abstract public function increment(string $key, string $field, int $ttlSeconds): int; + /** + * Read and decode the JSON contents of the store file. + * + * Decodes the file contents as an associative array; returns an empty array if the file is empty. + * + * @return array The decoded data as an associative array, or an empty array when the file has no content. + */ protected function read(): array { $data = file_get_contents($this->path); @@ -38,8 +66,15 @@ protected function read(): array return $data ? json_decode($data, true) : []; } + /** + * Writes the provided data to the storage file as pretty-printed JSON. + * + * Encodes `$data` to JSON using `JSON_PRETTY_PRINT` and overwrites the file at `$this->path`. + * + * @param array $data The data to encode and store. + */ protected function write(array $data): void { file_put_contents($this->path, json_encode($data, \JSON_PRETTY_PRINT)); } -} +} \ No newline at end of file diff --git a/lib/RateLimit/RateLimitStoreInterface.php b/lib/RateLimit/RateLimitStoreInterface.php index e8d236e..9b110e5 100644 --- a/lib/RateLimit/RateLimitStoreInterface.php +++ b/lib/RateLimit/RateLimitStoreInterface.php @@ -18,20 +18,32 @@ interface RateLimitStoreInterface { /** - * Returns stored data for the given client identifier and window (second/day). - * Array: - * remaining => int - * timestamp => int (unix time when the value was received). - */ + * Retrieve stored rate-limit data for a client and window. + * + * @param string $clientId The client identifier. + * @param string $window The rate limit window (e.g., "second" or "day"). + * @return array|null An array with keys `remaining` (int) and `timestamp` (int, Unix time) or `null` if no data is stored. + */ public function get(string $clientId, string $window): ?array; /** - * Stores data for the client and window (second/day). - */ + * Store rate-limit data for a client and time window. + * + * Stores the number of remaining requests and the associated Unix timestamp for + * the specified client identifier and window (e.g., "second" or "day"). + * + * @param string $clientId Identifier of the client. + * @param string $window Time window name (for example "second" or "day"). + * @param int $remaining Number of remaining requests for the client in this window. + * @param int $timestamp Unix timestamp representing when the stored data expires or was recorded. + */ public function set(string $clientId, string $window, int $remaining, int $timestamp): void; /** - * Returns all data for one client (for debugging). - */ + * Retrieve stored rate-limit data for the given client (intended for debugging). + * + * @param string $clientId The client identifier. + * @return array An associative array keyed by window name (e.g., 'second', 'day') where each value is an array with keys `remaining` (int) and `timestamp` (int, Unix time). Returns an empty array if no data exists for the client. + */ public function allForClient(string $clientId): array; -} +} \ No newline at end of file diff --git a/lib/RateLimit/RateLimiter.php b/lib/RateLimit/RateLimiter.php index 50f4860..8f720f7 100644 --- a/lib/RateLimit/RateLimiter.php +++ b/lib/RateLimit/RateLimiter.php @@ -20,19 +20,34 @@ class RateLimiter private RateLimitStoreInterface $store; private bool $waitMode; + /** + * Create a RateLimiter configured with a storage backend and a handling mode for exceeded limits. + * + * @param RateLimitStoreInterface $store Storage backend for per-client rate-limit state. + * @param bool $waitMode If true, the limiter will wait until the limit window resets; if false, it will throw a RateLimitExceededException when limits are exceeded. + */ public function __construct(RateLimitStoreInterface $store, bool $waitMode = true) { $this->store = $store; $this->waitMode = $waitMode; } + /** + * Indicates whether the limiter is configured to wait when a rate limit is exceeded. + * + * @return bool `true` if the limiter waits until the rate-limit window expires, `false` otherwise. + */ public function isWaitMode(): bool { return $this->waitMode; } /** - * clientId = fingerprint of the certificate (sha1, serial+issuer, etc.) - * secondLimits/dayLimits are obtained from API response headers. + * Stores remaining rate-limit counts for a client for both the one-second and 24-hour windows. + * + * @param string $clientId Fingerprint identifying the client (e.g., certificate SHA1, serial+issuer). + * @param int $remainingSecond Remaining requests in the current one-second window. + * @param int $remainingDay Remaining requests in the current 24-hour window. + * @param int $timestamp UNIX timestamp (seconds) when the limits were observed. */ public function handleRateLimits( string $clientId, @@ -45,7 +60,13 @@ public function handleRateLimits( } /** - * Verification before the next request. + * Ensures the client is allowed to make the next request by enforcing per-second and per-day rate limits. + * + * If a window is exhausted and wait mode is enabled, pauses execution for the required seconds to clear the window; + * otherwise throws a RateLimitExceededException. + * + * @param string $clientId Identifier of the client whose rate limits are checked. + * @throws RateLimitExceededException If a rate limit is exceeded and wait mode is disabled. */ public function checkBeforeRequest(string $clientId): void { @@ -82,4 +103,4 @@ public function checkBeforeRequest(string $clientId): void } } } -} +} \ No newline at end of file diff --git a/lib/RateLimit/SqlDialect.php b/lib/RateLimit/SqlDialect.php index 88dca4d..6e02404 100644 --- a/lib/RateLimit/SqlDialect.php +++ b/lib/RateLimit/SqlDialect.php @@ -17,6 +17,16 @@ interface SqlDialect { - public function now(): int; // PHP timestamp → we store it directly as int + /** + * Get the current time as a Unix timestamp. + * + * @return int The current Unix timestamp (seconds since the Unix epoch). + */ +public function now(): int; /** + * Generate a SQL parameter placeholder for the given parameter name. + * + * @param string $name Logical parameter name (without any placeholder prefix characters). + * @return string The SQL placeholder string to use in queries (for example `:name`, `?`, or `@name` depending on the dialect). + */ public function placeholder(string $name): string; // :name, ?, @name ... -} +} \ No newline at end of file