<?php
namespace SHServ\Tools;
class RateLimiter {
protected int $maxRequests;
protected int $windowSeconds;
protected string $storagePath;
public function __construct(int $maxRequests = 60, int $windowSeconds = 60, ?string $storagePath = null) {
$this -> maxRequests = $maxRequests;
$this -> windowSeconds = $windowSeconds;
$this -> storagePath = $storagePath ?? dirname(__DIR__, 2) . '/Cache/rate-limit.json';
$this -> ensureStorage();
}
protected function ensureStorage(): void {
$dir = dirname($this -> storagePath);
if (!is_dir($dir)) {
mkdir($dir, 0750, true);
}
if (!file_exists($this -> storagePath)) {
file_put_contents($this -> storagePath, '{}');
chmod($this -> storagePath, 0640);
}
}
public function check(String $key): bool {
if (extension_loaded('apcu') && apcu_enabled()) {
return $this -> checkApcu($key);
}
return $this -> checkFile($key);
}
protected function checkApcu(String $key): bool {
$cacheKey = 'rate_limit:' . $key;
$requests = apcu_fetch($cacheKey);
if ($requests === false) {
$requests = [];
}
$now = time();
$requests = array_filter($requests, function($ts) use ($now) {
return $now - $ts < $this -> windowSeconds;
});
if (count($requests) >= $this -> maxRequests) {
apcu_store($cacheKey, array_values($requests), $this -> windowSeconds);
return false;
}
$requests[] = $now;
apcu_store($cacheKey, array_values($requests), $this -> windowSeconds);
return true;
}
protected function checkFile(String $key): bool {
$fp = fopen($this -> storagePath, 'c+');
if (!$fp) {
return true;
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
return true;
}
$data = json_decode(stream_get_contents($fp), true) ?: [];
$now = time();
if (!isset($data[$key])) {
$data[$key] = [];
}
$data[$key] = array_values(array_filter($data[$key], function($ts) use ($now) {
return $now - $ts < $this -> windowSeconds;
}));
$allowed = count($data[$key]) < $this -> maxRequests;
if ($allowed) {
$data[$key][] = $now;
}
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($data));
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return $allowed;
}
public function clear(): void {
if (extension_loaded('apcu') && apcu_enabled()) {
apcu_delete('rate_limit:');
return;
}
$fp = fopen($this -> storagePath, 'c+');
if (!$fp) {
return;
}
if (flock($fp, LOCK_EX)) {
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, '{}');
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
}
}