From bf2f1e0a5957d0056c2ea28d5d21595e6f8f51ff Mon Sep 17 00:00:00 2001 From: Ismail Aatif <58871886+ismailian@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:33:00 +0100 Subject: [PATCH] refactor storage drivers across session cache and throttle --- System/Cache/Cache.php | 3 +- System/Cache/Drivers/DbDriver.php | 38 +++++ System/Cache/Drivers/FileDriver.php | 67 ++------ System/Cache/Drivers/RedisDriver.php | 76 ++------- System/Drivers/DatabaseDriver.php | 156 ++++++++++++++++++ System/Drivers/FilesystemDriver.php | 163 +++++++++++++++++++ System/Drivers/RedisDriver.php | 114 +++++++++++++ System/Drivers/StoreDriver.php | 58 +++++++ System/Session/Drivers/DbDriver.php | 88 +++------- System/Session/Drivers/FileDriver.php | 77 ++------- System/Session/Drivers/RedisDriver.php | 74 +++------ System/Throttle/Drivers/DatabaseDriver.php | 135 +++++---------- System/Throttle/Drivers/FilesystemDriver.php | 158 +++++------------- System/Throttle/Drivers/RedisDriver.php | 95 ++--------- 14 files changed, 699 insertions(+), 603 deletions(-) create mode 100644 System/Cache/Drivers/DbDriver.php create mode 100644 System/Drivers/DatabaseDriver.php create mode 100644 System/Drivers/FilesystemDriver.php create mode 100644 System/Drivers/RedisDriver.php create mode 100644 System/Drivers/StoreDriver.php diff --git a/System/Cache/Cache.php b/System/Cache/Cache.php index 1811a94..76a21f5 100644 --- a/System/Cache/Cache.php +++ b/System/Cache/Cache.php @@ -12,7 +12,7 @@ use TeleBot\System\Core\Enums\DataSource; use TeleBot\System\Interfaces\ICacheDriver; -use TeleBot\System\Cache\Drivers\{FileDriver, RedisDriver}; +use TeleBot\System\Cache\Drivers\{DbDriver, FileDriver, RedisDriver}; class Cache { @@ -31,6 +31,7 @@ private function init(): ICacheDriver $driver = env('CACHE_DRIVER', DataSource::FILESYSTEM); $this->client = match ($driver) { DataSource::REDIS => new RedisDriver(), + DataSource::DATABASE => new DbDriver(), DataSource::FILESYSTEM => new FileDriver(), }; } diff --git a/System/Cache/Drivers/DbDriver.php b/System/Cache/Drivers/DbDriver.php new file mode 100644 index 0000000..ba5b906 --- /dev/null +++ b/System/Cache/Drivers/DbDriver.php @@ -0,0 +1,38 @@ +store = new Store('cache', 'cache_key', 'cache_value', 'ttl', true); + } + + public function getAll(int $cursor = 0, int $count = 100): array + { + return $this->store->getAll($cursor, $count); + } + + public function read(string $key): mixed + { + return $this->store->get($key); + } + + public function write(string $key, mixed $data, ?string $ttl = null): bool + { + $ttl = $ttl ? iso8601_to_seconds($ttl) : null; + return $this->store->set($key, $data, $ttl); + } + + public function delete(string $key): bool + { + return (bool)$this->store->delete($key); + } +} diff --git a/System/Cache/Drivers/FileDriver.php b/System/Cache/Drivers/FileDriver.php index c6cf7fa..0ff0bc6 100644 --- a/System/Cache/Drivers/FileDriver.php +++ b/System/Cache/Drivers/FileDriver.php @@ -10,78 +10,37 @@ namespace TeleBot\System\Cache\Drivers; -use TeleBot\System\Core\Traits\Expirable; +use TeleBot\System\Drivers\FilesystemDriver as Store; use TeleBot\System\Interfaces\ICacheDriver; class FileDriver implements ICacheDriver { + /** @var Store $store */ + private Store $store; - use Expirable; + public function __construct() + { + $this->store = new Store(env('CACHE_DIR')); + } - /** - * @inheritDoc - */ public function getAll(int $cursor = 0, int $count = 100): array { - $dir = implode('/', [env('CACHE_DIR'), '*']); - return array_filter(glob($dir) ?? [], 'is_file'); + return $this->store->getAll($cursor, $count); } - /** - * @inheritDoc - */ public function read(string $key): mixed { - $cachePath = env('CACHE_DIR'); - $cacheFilePath = $cachePath . '/' . $key; - if (!file_exists($cacheFilePath)) { - return null; - } - - $content = file_get_contents($cacheFilePath); - if ($content === false) { - return null; - } - - if (($json = json_decode($content, true))) { - if ($this->hasExpired($json)) { - $this->delete($key); - return null; - } - - $content = $this->restore($json); - } - - return $content; + return $this->store->get($key); } - /** - * @inheritDoc - */ public function write(string $key, mixed $data, ?string $ttl = null): bool { - $data = [ - self::TTL_KEY => $ttl ? iso8601_to_timestamp($ttl) : null, - self::CONTENT_KEY => $data, - ]; - - $cachePath = env('CACHE_DIR'); - $cacheFilePath = $cachePath . '/' . $key; - return (bool)file_put_contents($cacheFilePath, json_encode($data)); + $ttl = $ttl ? iso8601_to_seconds($ttl) : null; + return $this->store->set($key, $data, $ttl); } - /** - * @inheritDoc - */ public function delete(string $key): bool { - $cachePath = env('CACHE_DIR'); - $cacheFilePath = $cachePath . '/' . $key; - if (file_exists($cacheFilePath)) { - return @unlink($cacheFilePath); - } - - return true; + return (bool)$this->store->delete($key); } - -} \ No newline at end of file +} diff --git a/System/Cache/Drivers/RedisDriver.php b/System/Cache/Drivers/RedisDriver.php index 8309c97..8b51255 100644 --- a/System/Cache/Drivers/RedisDriver.php +++ b/System/Cache/Drivers/RedisDriver.php @@ -10,25 +10,16 @@ namespace TeleBot\System\Cache\Drivers; -use Predis\Client; +use TeleBot\System\Drivers\RedisDriver as Store; use TeleBot\System\Exceptions\MissingToken; use TeleBot\System\Interfaces\ICacheDriver; class RedisDriver implements ICacheDriver { - - /** @var Client $client redis client */ - protected Client $client; - - /** @var mixed $cache cache value of cache content */ - protected mixed $cache = []; - - /** @var string $prefix redis key prefix */ - protected string $prefix = 'tg:bots'; + /** @var Store $store */ + private Store $store; /** - * default constructor - * * @throws MissingToken */ public function __construct() @@ -38,74 +29,27 @@ public function __construct() } $botId = explode(':', $botToken, 2)[0]; - $this->prefix = "tg:bots:$botId:cache"; - $this->client = new Client([ - 'scheme' => 'tcp', - 'host' => env('REDIS_HOST'), - 'port' => env('REDIS_PORT'), - 'user' => env('REDIS_USER'), - 'password' => env('REDIS_PASSWORD') - ]); + $this->store = new Store("tg:bots:{$botId}:cache"); } - /** - * @inheritDoc - */ public function getAll(int $cursor = 0, int $count = 100): array { - $keys = []; - do { - $options = [ - 'count' => $count, - 'match' => $this->prefix . ':*', - ]; - - [$page, $_keys] = $this->client->scan($cursor, $options); - if (!empty($_keys)) { - $keys = array_merge($keys, $_keys); - } - } while ($page !== '0'); - return $keys; + return $this->store->getAll($cursor, $count); } - /** - * @inheritDoc - */ public function read(string $key): mixed { - if (empty($this->cache)) { - $data = $this->client->get("$this->prefix:$key"); - if (!empty($data) && ($json = json_decode($data, true))) { - $this->cache = $json; - } - } - - return $this->cache; + return $this->store->get($key); } - /** - * @inheritDoc - */ public function write(string $key, mixed $data, ?string $ttl = null): bool { - $this->cache = $data; - if (is_array($data) || is_object($data)) { - $data = json_encode($data); - } - - return !!$this->client->set( - "$this->prefix:$key", - $data, - ($ttl ? 'EX' : null), - ($ttl ? iso8601_to_seconds($ttl) : null) - ); + $ttl = $ttl ? iso8601_to_seconds($ttl) : null; + return $this->store->set($key, $data, $ttl); } - /** - * @inheritDoc - */ public function delete(string $key): bool { - return $this->client->del("$this->prefix:$key"); + return (bool)$this->store->delete($key); } -} \ No newline at end of file +} diff --git a/System/Drivers/DatabaseDriver.php b/System/Drivers/DatabaseDriver.php new file mode 100644 index 0000000..1a91541 --- /dev/null +++ b/System/Drivers/DatabaseDriver.php @@ -0,0 +1,156 @@ +createTable) { + $this->ensureTable(); + } + } + + /** + * @inheritDoc + */ + public function getAll(int $cursor = 0, int $count = 100): array + { + return database()->rows("SELECT * FROM {$this->table} LIMIT {$count}") ?: []; + } + + /** + * @inheritDoc + */ + public function get(string $key): mixed + { + $row = $this->row($key); + if (!$row) { + return null; + } + + if (!empty($row[$this->ttlColumn]) && (int)$row[$this->ttlColumn] <= time()) { + $this->delete($key); + return null; + } + + return $this->decode($row[$this->valueColumn] ?? null); + } + + /** + * @inheritDoc + */ + public function set(string $key, mixed $value, ?int $ttl = null): bool + { + $expiresAt = $ttl ? time() + $ttl : null; + $value = $this->encode($value); + + $stmt = database()->run( + "INSERT INTO {$this->table} ({$this->keyColumn}, {$this->valueColumn}, {$this->ttlColumn}) + VALUES (:key, :value, :ttl) + ON DUPLICATE KEY UPDATE + {$this->valueColumn} = VALUES({$this->valueColumn}), + {$this->ttlColumn} = VALUES({$this->ttlColumn})", + [ + 'key' => $key, + 'value' => $value, + 'ttl' => $expiresAt, + ] + ); + + return (bool)$stmt; + } + + /** + * @inheritDoc + */ + public function delete(string $key): int|bool + { + return database()->delete($this->table, [$this->keyColumn => $key]); + } + + /** + * @inheritDoc + */ + public function increment(string $key, int $ttl): int + { + $now = time(); + $expiresAt = $now + $ttl; + + database()->run( + "INSERT INTO {$this->table} ({$this->keyColumn}, {$this->valueColumn}, {$this->ttlColumn}) + VALUES (:key, :value1, :ttl1) + ON DUPLICATE KEY UPDATE + {$this->valueColumn} = IF({$this->ttlColumn} <= :now, :value2, CAST({$this->valueColumn} AS UNSIGNED) + 1), + {$this->ttlColumn} = IF({$this->ttlColumn} <= :now, :ttl2, {$this->ttlColumn})", + [ + 'key' => $key, + 'value1' => 1, + 'value2' => 1, + 'ttl1' => $expiresAt, + 'ttl2' => $expiresAt, + 'now' => $now, + ] + ); + + return (int)($this->get($key) ?? 0); + } + + /** + * @inheritDoc + */ + public function ttl(string $key): int + { + $row = $this->row($key); + if (!$row || empty($row[$this->ttlColumn])) { + return 0; + } + + return max(0, (int)$row[$this->ttlColumn] - time()); + } + + private function row(string $key): array|object|bool + { + return database()->row( + "SELECT {$this->valueColumn}, {$this->ttlColumn} FROM {$this->table} WHERE {$this->keyColumn} = :key", + ['key' => $key] + ); + } + + private function encode(mixed $value): string|int|float|null + { + if (is_array($value) || is_object($value) || is_bool($value) || $value === null) { + return json_encode($value); + } + + return $value; + } + + private function decode(mixed $value): mixed + { + if (!is_string($value)) { + return $value; + } + + $json = json_decode($value, true); + return $json === null && $value !== 'null' ? $value : $json; + } + + private function ensureTable(): void + { + database()->getClient()->exec( + "CREATE TABLE IF NOT EXISTS `{$this->table}` ( + `{$this->keyColumn}` VARCHAR(255) NOT NULL, + `{$this->valueColumn}` TEXT NULL, + `{$this->ttlColumn}` INT NULL, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`{$this->keyColumn}`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + ); + } +} diff --git a/System/Drivers/FilesystemDriver.php b/System/Drivers/FilesystemDriver.php new file mode 100644 index 0000000..6b7a133 --- /dev/null +++ b/System/Drivers/FilesystemDriver.php @@ -0,0 +1,163 @@ +directory = rtrim($directory, '/'); + $this->extension = $extension; + + if (!is_dir($this->directory)) { + mkdir($this->directory, 0755, true); + } + } + + /** + * @inheritDoc + */ + public function getAll(int $cursor = 0, int $count = 100): array + { + $dir = $this->directory . '/*' . $this->extension; + return array_filter(glob($dir) ?: [], 'is_file'); + } + + /** + * @inheritDoc + */ + public function get(string $key): mixed + { + $file = $this->filePath($key); + if (!file_exists($file)) { + return null; + } + + $contents = file_get_contents($file); + $payload = $contents ? json_decode($contents, true) : null; + if (!is_array($payload)) { + return null; + } + + if (!empty($payload['expires_at']) && time() > (int)$payload['expires_at']) { + $this->delete($key); + return null; + } + + return $payload['value'] ?? null; + } + + /** + * @inheritDoc + */ + public function set(string $key, mixed $value, ?int $ttl = null): bool + { + $payload = [ + 'expires_at' => $ttl ? (time() + $ttl) : null, + 'value' => $value, + ]; + + return (bool)file_put_contents($this->filePath($key), json_encode($payload), LOCK_EX); + } + + /** + * @inheritDoc + */ + public function delete(string $key): bool + { + $file = $this->filePath($key); + if (file_exists($file)) { + return @unlink($file); + } + + return true; + } + + /** + * @inheritDoc + */ + public function increment(string $key, int $ttl): int + { + $data = $this->getCounter($key, $ttl); + $data['count']++; + $this->set($key, $data, max(1, $data['expires_at'] - time())); + return $data['count']; + } + + /** + * @inheritDoc + */ + public function ttl(string $key): int + { + $payload = $this->payload($key); + if (!$payload || empty($payload['expires_at'])) { + return 0; + } + + return max(0, (int)$payload['expires_at'] - time()); + } + + /** + * @param string $key + * @return array|null + */ + private function payload(string $key): ?array + { + $file = $this->filePath($key); + if (!file_exists($file)) { + return null; + } + + $contents = file_get_contents($file); + $payload = $contents ? json_decode($contents, true) : null; + if (!is_array($payload)) { + return null; + } + + if (!empty($payload['expires_at']) && time() > (int)$payload['expires_at']) { + $this->delete($key); + return null; + } + + return $payload; + } + + /** + * @param string $key + * @param int $ttl + * @return array + */ + private function getCounter(string $key, int $ttl): array + { + $payload = $this->payload($key); + $value = $payload['value'] ?? null; + + if (!is_array($value) || !isset($value['count'], $value['expires_at']) || time() > (int)$value['expires_at']) { + return [ + 'count' => 0, + 'expires_at' => time() + $ttl, + ]; + } + + return $value; + } + + /** + * @param string $key + * @return string + */ + private function filePath(string $key): string + { + return $this->directory . '/' . sha1($key) . $this->extension; + } +} diff --git a/System/Drivers/RedisDriver.php b/System/Drivers/RedisDriver.php new file mode 100644 index 0000000..2636b08 --- /dev/null +++ b/System/Drivers/RedisDriver.php @@ -0,0 +1,114 @@ +prefix = rtrim($prefix, ':'); + $this->client = new Client([ + 'scheme' => 'tcp', + 'host' => env('REDIS_HOST'), + 'port' => env('REDIS_PORT'), + 'user' => env('REDIS_USER'), + 'password' => env('REDIS_PASSWORD') + ]); + } + + /** + * @inheritDoc + */ + public function getAll(int $cursor = 0, int $count = 100): array + { + $keys = []; + do { + [$page, $_keys] = $this->client->scan($cursor, [ + 'count' => $count, + 'match' => $this->prefix . ':*', + ]); + + if (!empty($_keys)) { + $keys = array_merge($keys, $_keys); + } + + $cursor = (int)$page; + } while ((string)$page !== '0'); + + return $keys; + } + + /** + * @inheritDoc + */ + public function get(string $key): mixed + { + $data = $this->client->get($this->key($key)); + if ($data === null) { + return null; + } + + $json = json_decode($data, true); + return $json === null && $data !== 'null' ? $data : $json; + } + + /** + * @inheritDoc + */ + public function set(string $key, mixed $value, ?int $ttl = null): bool + { + if (is_array($value) || is_object($value) || $value === null || is_bool($value)) { + $value = json_encode($value); + } + + if ($ttl !== null) { + return (bool)$this->client->set($this->key($key), $value, 'EX', $ttl); + } + + return (bool)$this->client->set($this->key($key), $value); + } + + /** + * @inheritDoc + */ + public function delete(string $key): int + { + return $this->client->del($this->key($key)); + } + + /** + * @inheritDoc + */ + public function increment(string $key, int $ttl): int + { + $redisKey = $this->key($key); + $count = $this->client->incr($redisKey); + if ($count === 1) { + $this->client->expire($redisKey, $ttl); + } + + return (int)$count; + } + + /** + * @inheritDoc + */ + public function ttl(string $key): int + { + $ttl = (int)$this->client->ttl($this->key($key)); + return max(0, $ttl); + } + + private function key(string $key): string + { + return $this->prefix . ':' . $key; + } +} diff --git a/System/Drivers/StoreDriver.php b/System/Drivers/StoreDriver.php new file mode 100644 index 0000000..8c27468 --- /dev/null +++ b/System/Drivers/StoreDriver.php @@ -0,0 +1,58 @@ +sessionId = $sessionId; - if (!database()->row("SELECT id FROM `sessions` WHERE `session_id` = ?", [$sessionId])) { - database()->insert(self::SESSION_TABLE, [ - self::SESSION_ID_KEY => $sessionId, - self::SESSION_DATA_KEY => json_encode([]) - ]); - } + $this->store = new Store('sessions', 'session_id', 'data', 'ttl'); + $this->key = $sessionId; } - /** - * @inheritDoc - */ public function getAll(int $cursor = 0, int $count = 100): array { - return database()->rows('SELECT * FROM ' . self::SESSION_TABLE) ?? []; + return $this->store->getAll($cursor, $count); } - /** - * @inheritDoc - */ public function read(): array { if (empty($this->cached)) { - $session = database()->row("SELECT ttl,data FROM `sessions` WHERE `session_id` = ?", [$this->sessionId]); - if (!empty($session)) { - $this->cached[self::SESSION_DATA_KEY] = $session[self::SESSION_DATA_KEY] ?? []; - $this->cached[self::TTL_KEY] = $session[self::TTL_KEY] ?? null; + $value = $this->store->get($this->key); + if (is_array($value)) { + $this->cached = $value; } } - if (!empty($this->cached[self::TTL_KEY]) && (int)$this->cached[self::TTL_KEY] < time()) { - database()->delete(self::SESSION_TABLE, [ - self::SESSION_ID_KEY => $this->sessionId, - ]); - - return $this->cached = []; - } - - $data = $this->cached[self::SESSION_DATA_KEY] ?? []; - if (!empty($data) && !is_array($data) && ($json = json_decode($data, true)) !== null) { - $data = $json; - } - - return $data; + return $this->cached; } - /** - * @inheritDoc - */ public function write(array $data, ?string $ttl = null): bool { - $sessData = [ - self::SESSION_DATA_KEY => json_encode($data), - self::TTL_KEY => $ttl ? iso8601_to_timestamp($ttl) : null, - ]; - - $this->cached = $sessData; - return database()->update(self::SESSION_TABLE, $sessData, - [self::SESSION_ID_KEY => $this->sessionId] - ); + $this->cached = $data; + $ttl = $ttl ? iso8601_to_seconds($ttl) : null; + return $this->store->set($this->key, $data, $ttl); } - /** - * @inheritDoc - */ public function delete(): int|bool { - return database()->delete(self::SESSION_TABLE, [ - self::SESSION_ID_KEY => $this->sessionId - ]); + $this->cached = []; + return $this->store->delete($this->key); } -} \ No newline at end of file +} diff --git a/System/Session/Drivers/FileDriver.php b/System/Session/Drivers/FileDriver.php index 5870da9..cf430cb 100644 --- a/System/Session/Drivers/FileDriver.php +++ b/System/Session/Drivers/FileDriver.php @@ -10,94 +10,53 @@ namespace TeleBot\System\Session\Drivers; -use TeleBot\System\Core\Traits\Expirable; +use TeleBot\System\Drivers\FilesystemDriver as Store; use TeleBot\System\Interfaces\ISessionDriver; class FileDriver implements ISessionDriver { + /** @var Store $store */ + private Store $store; - use Expirable; + /** @var string $key */ + private string $key; - /** @var string $sessionId session id */ - private string $sessionId; - - /** @var string $sessionFilePath session file path */ - private string $sessionFilePath; - - /** @var array $cached cached session content for quick access */ + /** @var array $cached */ private array $cached = []; - /** - * @inheritDoc - */ public function __construct(string $sessionId) { - $sessDir = env('SESSION_DIR', 'session'); - $sessName = md5($sessionId) . '.json'; - $sessData = [ - self::TTL_KEY => null, - self::CONTENT_KEY => [], - ]; - - $this->sessionId = $sessionId; - $this->sessionFilePath = join('/', [$sessDir, $sessName]); - if (!file_exists($this->sessionFilePath)) { - if (!file_exists(dirname($this->sessionFilePath))) { - @mkdir(dirname($this->sessionFilePath)); - } - - file_put_contents($this->sessionFilePath, json_encode($sessData)); - } + $this->store = new Store(env('SESSION_DIR', 'session')); + $this->key = md5($sessionId); } - /** - * @inheritDoc - */ public function getAll(int $cursor = 0, int $count = 100): array { - $dir = implode('/', [env('SESSION_DIR'), '*']); - return array_filter(glob($dir) ?? [], 'is_file'); + return $this->store->getAll($cursor, $count); } - /** - * @inheritDoc - */ public function read(): array { if (empty($this->cached)) { - $content = file_get_contents($this->sessionFilePath); - if ($json = json_decode($content, true)) { - $this->cached = $json; + $value = $this->store->get($this->key); + if (is_array($value)) { + $this->cached = $value; } } - if ($this->hasExpired($this->cached)) { - $this->delete(); - return $this->cached = []; - } - - return $this->restore($this->cached); + return $this->cached; } - /** - * @inheritDoc - */ public function write(array $data, ?string $ttl = null): bool { - $data = [ - self::TTL_KEY => $ttl ? iso8601_to_timestamp($ttl) : null, - self::CONTENT_KEY => $data, - ]; - $this->cached = $data; - return (bool)file_put_contents($this->sessionFilePath, json_encode($data)); + $ttl = $ttl ? iso8601_to_seconds($ttl) : null; + return $this->store->set($this->key, $data, $ttl); } - /** - * @inheritDoc - */ public function delete(): int|bool { - return @unlink($this->sessionFilePath); + $this->cached = []; + return $this->store->delete($this->key); } -} \ No newline at end of file +} diff --git a/System/Session/Drivers/RedisDriver.php b/System/Session/Drivers/RedisDriver.php index 2b893c7..c01eeb4 100644 --- a/System/Session/Drivers/RedisDriver.php +++ b/System/Session/Drivers/RedisDriver.php @@ -10,27 +10,22 @@ namespace TeleBot\System\Session\Drivers; -use Predis\Client; +use TeleBot\System\Drivers\RedisDriver as Store; use TeleBot\System\Exceptions\MissingToken; use TeleBot\System\Interfaces\ISessionDriver; class RedisDriver implements ISessionDriver { + /** @var Store $store */ + private Store $store; - /** @var Client $client redis client */ - private Client $client; + /** @var string $key */ + private string $key; - /** @var string $sessionId session id */ - private string $sessionId; - - /** @var array $cached cached session content for quick access */ + /** @var array $cached */ private array $cached = []; - /** @var string $prefix redis key prefix */ - private string $prefix = 'tg:bots'; - /** - * @inheritDoc * @throws MissingToken */ public function __construct(string $sessionId) @@ -39,69 +34,38 @@ public function __construct(string $sessionId) throw new MissingToken; } - $this->prefix = 'tg:bots:' . explode(':', $botToken, 2)[0]; - $this->sessionId = $sessionId; - $this->client = new Client([ - 'scheme' => 'tcp', - 'host' => env('REDIS_HOST'), - 'port' => env('REDIS_PORT'), - 'user' => env('REDIS_USER'), - 'password' => env('REDIS_PASSWORD') - ]); + $botId = explode(':', $botToken, 2)[0]; + $this->store = new Store("tg:bots:{$botId}:session"); + $this->key = $sessionId; } - /** - * @inheritDoc - */ public function getAll(int $cursor = 0, int $count = 100): array { - $keys = []; - do { - $options = [ - 'count' => $count, - 'match' => $this->prefix . ':*', - ]; - - [$page, $_keys] = $this->client->scan($cursor, $options); - if (!empty($_keys)) { - $keys = array_merge($keys, $_keys); - } - } while ($page !== '0'); - return $keys; + return $this->store->getAll($cursor, $count); } - /** - * @inheritDoc - */ public function read(): array { if (empty($this->cached)) { - $data = $this->client->get("$this->prefix:{$this->sessionId}"); - if (!empty($data) && ($json = json_decode($data, true))) { - $this->cached = $json; + $value = $this->store->get($this->key); + if (is_array($value)) { + $this->cached = $value; } } return $this->cached; } - /** - * @inheritDoc - */ public function write(array $data, ?string $ttl = null): bool { $this->cached = $data; - return !!$this->client->set( - "$this->prefix:$this->sessionId", json_encode($data), - ($ttl ? 'EX' : null), ($ttl ? iso8601_to_seconds($ttl) : null) - ); + $ttl = $ttl ? iso8601_to_seconds($ttl) : null; + return $this->store->set($this->key, $data, $ttl); } - /** - * @inheritDoc - */ - public function delete(): int + public function delete(): int|bool { - return $this->client->del("$this->prefix:$this->sessionId"); + $this->cached = []; + return $this->store->delete($this->key); } -} \ No newline at end of file +} diff --git a/System/Throttle/Drivers/DatabaseDriver.php b/System/Throttle/Drivers/DatabaseDriver.php index a6f1a98..60669a1 100644 --- a/System/Throttle/Drivers/DatabaseDriver.php +++ b/System/Throttle/Drivers/DatabaseDriver.php @@ -1,98 +1,37 @@ -ensureTable(); - } - - /** - * @inheritDoc - */ - public function get(string $key): int - { - return database()->count( - "SELECT count FROM {$this->table} WHERE cache_key = :key AND expires_at > :exp", - [ - 'key' => $key, - 'exp' => time() - ] - ); - } - - /** - * @inheritDoc - */ - public function increment(string $key, int $ttl): int - { - $expiresAt = time() + $ttl; - database()->run( - "INSERT INTO {$this->table} (cache_key, count, expires_at) - VALUES (:key, 1, :exp1) - ON DUPLICATE KEY UPDATE - count = IF(expires_at <= UNIX_TIMESTAMP(), 1, count + 1), - expires_at = IF(expires_at <= UNIX_TIMESTAMP(), :exp2, expires_at)", - [ - 'key' => $key, - 'exp1' => $expiresAt, - 'exp2' => $expiresAt, - ] - ); - - return $this->get($key); - } - - /** - * @inheritDoc - */ - public function ttl(string $key): int - { - $result = database()->row( - "SELECT expires_at FROM {$this->table} WHERE cache_key = :key", - ['key' => $key] - ); - - if (!$result) { - return 0; - } - - return max(0, (int)$result['expires_at'] - time()); - } - - /** - * @inheritDoc - */ - public function reset(string $key): void - { - database()->delete($this->table, ['cache_key' => $key]); - } - - /** - * Create database table - * - * @return void - */ - private function ensureTable(): void - { - database()->getClient()->exec( - "CREATE TABLE IF NOT EXISTS `{$this->table}` ( - `cache_key` VARCHAR(255) NOT NULL, - `count` INT NOT NULL DEFAULT 1, - `expires_at` INT NOT NULL, - `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`cache_key`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - } -} \ No newline at end of file +store = new Store($this->table, 'cache_key', 'count', 'expires_at', true); + } + + public function get(string $key): int + { + return (int)($this->store->get($key) ?? 0); + } + + public function increment(string $key, int $ttl): int + { + return $this->store->increment($key, $ttl); + } + + public function ttl(string $key): int + { + return $this->store->ttl($key); + } + + public function reset(string $key): void + { + $this->store->delete($key); + } +} diff --git a/System/Throttle/Drivers/FilesystemDriver.php b/System/Throttle/Drivers/FilesystemDriver.php index 7d7d80f..f5b5305 100644 --- a/System/Throttle/Drivers/FilesystemDriver.php +++ b/System/Throttle/Drivers/FilesystemDriver.php @@ -1,121 +1,37 @@ -directory = env('THROTTLE_DIR'); - $this->directory = rtrim($this->directory, '/'); - - if (!is_dir($this->directory)) { - mkdir($this->directory, 0755, true); - } - } - - /** - * @inheritDoc - */ - public function get(string $key): int - { - $data = $this->read($key); - if ($data === null || time() > $data['expires_at']) { - return 0; - } - return $data['count']; - } - - /** - * @inheritDoc - */ - public function increment(string $key, int $ttl): int - { - $data = $this->read($key); - $now = time(); - - if ($data === null || $now > $data['expires_at']) { - $data = [ - 'count' => 0, - 'expires_at' => $now + $ttl, - ]; - } - - $data['count']++; - $this->write($key, $data); - - return $data['count']; - } - - /** - * @inheritDoc - */ - public function ttl(string $key): int - { - $data = $this->read($key); - if ($data === null) { - return 0; - } - return max(0, $data['expires_at'] - time()); - } - - /** - * @inheritDoc - */ - public function reset(string $key): void - { - $file = $this->filePath($key); - if (file_exists($file)) { - unlink($file); - } - } - - /** - * Get the file path - * - * @param string $key identity key - * @return string - */ - private function filePath(string $key): string - { - return $this->directory . '/' . sha1($key) . '.json'; - } - - /** - * Read data from a file - * - * @param string $key identity key - * @return array|null - */ - private function read(string $key): ?array - { - $file = $this->filePath($key); - if (!file_exists($file)) { - return null; - } - $contents = file_get_contents($file); - return $contents ? json_decode($contents, true) : null; - } - - /** - * Write data to a file - * - * @param string $key identity key - * @param array $data data to save - * @return void - */ - private function write(string $key, array $data): void - { - file_put_contents($this->filePath($key), json_encode($data), LOCK_EX); - } - -} \ No newline at end of file +store = new Store(env('THROTTLE_DIR')); + } + + public function get(string $key): int + { + return (int)($this->store->get($key) ?? 0); + } + + public function increment(string $key, int $ttl): int + { + return $this->store->increment($key, $ttl); + } + + public function ttl(string $key): int + { + return $this->store->ttl($key); + } + + public function reset(string $key): void + { + $this->store->delete($key); + } +} diff --git a/System/Throttle/Drivers/RedisDriver.php b/System/Throttle/Drivers/RedisDriver.php index aa9aff8..7874f02 100644 --- a/System/Throttle/Drivers/RedisDriver.php +++ b/System/Throttle/Drivers/RedisDriver.php @@ -1,34 +1,17 @@ prefix = "tg:bots:$botId:cache"; - $this->client = new Client([ - 'scheme' => 'tcp', - 'host' => env('REDIS_HOST'), - 'port' => env('REDIS_PORT'), - 'user' => env('REDIS_USER'), - 'password' => env('REDIS_PASSWORD') - ]); + $this->store = new Store("tg:bots:{$botId}:throttle"); } - /** - * @inheritDoc - */ - public function getAll(int $cursor = 0, int $count = 100): array + public function get(string $key): int { - $keys = []; - do { - $options = [ - 'count' => $count, - 'match' => $this->prefix . ':*', - ]; - - [$page, $_keys] = $this->client->scan($cursor, $options); - if (!empty($_keys)) { - $keys = array_merge($keys, $_keys); - } - } while ($page !== '0'); - return $keys; + return (int)($this->store->get($key) ?? 0); } - /** - * @inheritDoc - */ - public function read(string $key): mixed + public function increment(string $key, int $ttl): int { - if (empty($this->cache)) { - $data = $this->client->get("$this->prefix:$key"); - if (!empty($data) && ($json = json_decode($data, true))) { - $this->cache = $json; - } - } - - return $this->cache; + return $this->store->increment($key, $ttl); } - /** - * @inheritDoc - */ - public function write(string $key, mixed $data, ?string $ttl = null): bool + public function ttl(string $key): int { - $this->cache = $data; - if (is_array($data) || is_object($data)) { - $data = json_encode($data); - } - - return !!$this->client->set( - "$this->prefix:$key", - $data, - ($ttl ? 'EX' : null), - ($ttl ? iso8601_to_seconds($ttl) : null) - ); + return $this->store->ttl($key); } - /** - * @inheritDoc - */ - public function delete(string $key): bool + public function reset(string $key): void { - return $this->client->del("$this->prefix:$key"); + $this->store->delete($key); } -} \ No newline at end of file +}