* @license MIT License * @version GIT: * @link https://git.keithsolomon.net/keith/Spacetraders */ /** * Storage for configuration and API cache. */ class SpacetradersStorage { /** * Database handle. * * @var PDO */ private PDO $db; /** * Create storage and initialize schema. * * @param string $dbPath Absolute or project-relative SQLite database path. */ public function __construct( string $dbPath ) { $directory = dirname( $dbPath ); if (! is_dir( $directory ) ) { mkdir( $directory, 0777, true ); } $this->db = new PDO( 'sqlite:' . $dbPath ); $this->db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); $this->db->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC ); $this->_initializeSchema(); } /** * Create required tables if they do not exist. * * @return void */ private function _initializeSchema(): void { // phpcs:ignore $this->db->exec( 'CREATE TABLE IF NOT EXISTS settings ( setting_key TEXT PRIMARY KEY, setting_value TEXT NOT NULL, updated_at INTEGER NOT NULL )' ); $this->db->exec( 'CREATE TABLE IF NOT EXISTS api_cache ( cache_key TEXT PRIMARY KEY, response_json TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL )' ); $this->db->exec( 'CREATE INDEX IF NOT EXISTS idx_api_cache_expires ON api_cache (expires_at)' ); } /** * Save a generic setting. * * @param string $key Setting key. * @param string $value Setting value. * * @return void */ public function setSetting( string $key, string $value ): void { $stmt = $this->db->prepare( 'INSERT INTO settings (setting_key, setting_value, updated_at) VALUES (:key, :value, :updated_at) ON CONFLICT(setting_key) DO UPDATE SET setting_value = excluded.setting_value, updated_at = excluded.updated_at' ); $stmt->execute( array( ':key' => $key, ':value' => $value, ':updated_at' => time(), ) ); } /** * Load a generic setting. * * @param string $key Setting key. * * @return string|null */ public function getSetting( string $key ): ?string { $stmt = $this->db->prepare( 'SELECT setting_value FROM settings WHERE setting_key = :key LIMIT 1' ); $stmt->execute( array( ':key' => $key ) ); $row = $stmt->fetch(); if (! is_array( $row ) || ! isset( $row['setting_value'] ) ) { return null; } return (string) $row['setting_value']; } /** * Save the agent token in settings. * * @param string $token Agent token. * * @return void */ public function setAgentToken( string $token ): void { $this->setSetting( 'agent_token', $token ); } /** * Get the stored agent token, if available. * * @return string|null */ public function getAgentToken(): ?string { return $this->getSetting( 'agent_token' ); } /** * Fetch a valid cached response by cache key. * * @param string $cacheKey Cache key. * * @return array|null */ public function getCache( string $cacheKey ): ?array { $stmt = $this->db->prepare( 'SELECT response_json, expires_at FROM api_cache WHERE cache_key = :cache_key LIMIT 1' ); $stmt->execute( array( ':cache_key' => $cacheKey ) ); $row = $stmt->fetch(); if (! is_array( $row ) ) { return null; } if ((int) $row['expires_at'] <= time() ) { $this->deleteCache( $cacheKey ); return null; } $decoded = json_decode( (string) $row['response_json'], true ); if (! is_array( $decoded ) ) { $this->deleteCache( $cacheKey ); return null; } return $decoded; } /** * Store API response in cache. * * @param string $cacheKey Cache key. * @param array $response API response payload. * @param int $ttl Time to live in seconds. * * @return void */ public function setCache( string $cacheKey, array $response, int $ttl = 600 ): void { $json = json_encode( $response ); if ($json === false ) { return; } $createdAt = time(); $expiresAt = $createdAt + max( 1, $ttl ); $stmt = $this->db->prepare( 'INSERT INTO api_cache (cache_key, response_json, created_at, expires_at) VALUES (:cache_key, :response_json, :created_at, :expires_at) ON CONFLICT(cache_key) DO UPDATE SET response_json = excluded.response_json, created_at = excluded.created_at, expires_at = excluded.expires_at' ); $stmt->execute( array( ':cache_key' => $cacheKey, ':response_json' => $json, ':created_at' => $createdAt, ':expires_at' => $expiresAt, ) ); } /** * Remove one cache row. * * @param string $cacheKey Cache key. * * @return void */ public function deleteCache( string $cacheKey ): void { $stmt = $this->db->prepare( 'DELETE FROM api_cache WHERE cache_key = :cache_key' ); $stmt->execute( array( ':cache_key' => $cacheKey ) ); } /** * Remove all expired cache rows. * * @return void */ public function purgeExpiredCache(): void { $stmt = $this->db->prepare( 'DELETE FROM api_cache WHERE expires_at <= :now' ); $stmt->execute( array( ':now' => time() ) ); } /** * Remove all cached API rows. * * @return void */ public function clearAllCache(): void { $this->db->exec( 'DELETE FROM api_cache' ); } }