name = 'dbmemorycache'; $this->tab = 'administration'; $this->version = '1.2.0'; $this->author = 'YourName'; $this->need_instance = 0; $this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_]; $this->bootstrap = true; parent::__construct(); $this->displayName = $this->trans('MySQL Memory Cache Pro', [], 'Modules.Dbmemorycache.Admin'); $this->description = $this->trans('High-speed RAM cache. Auto-recovers after reboot.', [], 'Modules.Dbmemorycache.Admin'); $this->tableName = _DB_PREFIX_ . 'custom_mem_cache'; } public function install(): bool { // We create it here just to be sure, but the runtime logic handles it too. return parent::install() && $this->createCacheTable(); } public function uninstall(): bool { return $this->dropCacheTable() && parent::uninstall(); } /** * Lazy Initialization. * Checks/Creates table only when actually needed, not on module instantiation. */ private function initCache(): void { if (self::$isInitialized) { return; } $this->createCacheTable(); self::$isInitialized = true; } /** * Creates the MEMORY table if it doesn't exist. * Uses VARCHAR(128) for keys to allow "prefix_SHA256". */ private function createCacheTable(): bool { // MAX_ROWS is a hint to MySQL to allocate memory efficiently $sql = "CREATE TABLE IF NOT EXISTS `{$this->tableName}` ( `cache_key` VARCHAR(128) NOT NULL, `cache_value` VARCHAR(" . self::MAX_CACHE_SIZE_CHARS . ") NOT NULL, `expiry` INT(11) UNSIGNED NOT NULL, PRIMARY KEY (`cache_key`), INDEX `idx_expiry` (`expiry`) ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci MAX_ROWS=10000;"; return Db::getInstance()->execute($sql); } private function dropCacheTable(): bool { return Db::getInstance()->execute("DROP TABLE IF EXISTS `{$this->tableName}`"); } /** * Set a value in cache with "Table Full" protection */ public function setValue(string $key, $value, int $ttlSeconds = 3600): bool { $this->initCache(); // 1. Serialize $encodedValue = json_encode($value, JSON_UNESCAPED_UNICODE); if ($encodedValue === false) { return false; } // 2. Size Guard if (mb_strlen($encodedValue, 'UTF-8') > self::MAX_CACHE_SIZE_CHARS) { return false; } $expiry = time() + $ttlSeconds; $db = Db::getInstance(); $safeKey = pSQL($key); $safeValue = pSQL($encodedValue, true); // 3. Delete existing key (clean state) // We suppress errors here just in case table is weird state try { $db->execute("DELETE FROM `{$this->tableName}` WHERE `cache_key` = '$safeKey'"); } catch (Exception $e) { // If delete fails, table might be gone (manual drop?), try re-init $this->createCacheTable(); } // 4. Insert with Retry Logic for "Table Full" try { $sql = "INSERT IGNORE INTO `{$this->tableName}` (`cache_key`, `cache_value`, `expiry`) VALUES ('$safeKey', '$safeValue', $expiry)"; return $db->execute($sql); } catch (Exception $e) { // MySQL Error 1114 = The table is full // If the message contains "is full" or error code matches if (strpos($e->getMessage(), 'is full') !== false || $e->getCode() == 1114) { // EMERGENCY MEASURE: Prune expired keys $this->prune(); // Retry Insert once try { return $db->execute($sql); } catch (Exception $e2) { // Still full? The cache is saturated with valid data. // Option: Flush everything (Aggressive) OR Just return false (Conservative) // Let's return false to preserve existing valid cache. return false; } } // Log other errors PrestaShopLogger::addLog("DbMemoryCache SET Error: " . $e->getMessage(), 3); return false; } } public function getValue(string $key) { $this->initCache(); $db = Db::getInstance(); $safeKey = pSQL($key); $now = time(); try { $sql = "SELECT `cache_value` FROM `{$this->tableName}` WHERE `cache_key` = '$safeKey' AND `expiry` > $now"; $result = $db->getValue($sql); } catch (Exception $e) { // If table is missing (reboot happened during this request?), recreate and return null $this->createCacheTable(); return null; } if (!$result) { return null; } return json_decode($result, true); } public function existsValue(string $key): bool { $this->initCache(); $safeKey = pSQL($key); $now = time(); try { $sql = "SELECT 1 FROM `{$this->tableName}` WHERE `cache_key` = '$safeKey' AND `expiry` > $now"; return (bool) Db::getInstance()->getValue($sql); } catch (Exception $e) { return false; } } public function deleteValue(string $key): bool { $this->initCache(); $safeKey = pSQL($key); return Db::getInstance()->execute("DELETE FROM `{$this->tableName}` WHERE `cache_key` = '$safeKey'"); } public function prune(): bool { $this->initCache(); $now = time(); // Delete rows where expiry is in the past return Db::getInstance()->execute("DELETE FROM `{$this->tableName}` WHERE `expiry` <= $now"); } public function flush(): bool { $this->initCache(); return Db::getInstance()->execute("TRUNCATE TABLE `{$this->tableName}`"); } }