Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions lib/ApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [])
{
Expand Down Expand Up @@ -202,27 +205,25 @@ 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()
{
return substr(self::sourceString().'#'.time(), -59);
}

/**
* 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
{
Expand Down Expand Up @@ -253,4 +254,4 @@ public function send(\Psr\Http\Message\RequestInterface $request, array $options

return $response;
}
}
}
35 changes: 34 additions & 1 deletion lib/RateLimit/JsonRateLimitStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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] = [
Expand All @@ -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));
}
}
}
35 changes: 34 additions & 1 deletion lib/RateLimit/PdoRateLimitStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand All @@ -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<string, array{remaining:int,timestamp:int}> 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'
Expand All @@ -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'
Expand All @@ -88,4 +121,4 @@ private function init(): void

EOD);
}
}
}
41 changes: 38 additions & 3 deletions lib/RateLimit/RateLimitStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,18 +35,46 @@ 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);

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));
}
}
}
32 changes: 22 additions & 10 deletions lib/RateLimit/RateLimitStoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
29 changes: 25 additions & 4 deletions lib/RateLimit/RateLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
{
Expand Down Expand Up @@ -82,4 +103,4 @@ public function checkBeforeRequest(string $clientId): void
}
}
}
}
}
14 changes: 12 additions & 2 deletions lib/RateLimit/SqlDialect.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...
}
}