commit a3fbe97ddebb36edd00d6ca215e2da251ee07601 Author: O K Date: Wed Mar 4 14:10:12 2026 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..65e008d --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# DB Memory Cache for PrestaShop + +A high-performance caching module for PrestaShop 8 and 9 that utilizes the MySQL/MariaDB `MEMORY` (HEAP) engine. This module provides a fast, ephemeral key-value store directly in RAM without requiring external services like Redis or Memcached. + +## Features + +* **Speed:** Uses the `MEMORY` database engine to store data in RAM, avoiding disk I/O. +* **TTL Support:** Simulates Time-To-Live expiration for cached items. +* **Unicode Optimized:** Uses `JSON_UNESCAPED_UNICODE` to save space and support multi-byte characters efficiently. +* **Safety:** Automatically rejects data larger than the defined limit (~16KB) to prevent SQL errors or JSON truncation. +* **Strict Logic:** Ensures clean writes by deleting existing keys before insertion. + +## Requirements + +* PrestaShop 8.0.0 or higher (Compatible with PS 9). +* MySQL or MariaDB. +* The database user must have `CREATE` and `DROP` privileges (standard for PS). + +## Installation + +1. Create a folder named `dbmemorycache` in your PrestaShop `modules/` directory. +2. Upload `dbmemorycache.php` and `logo.png` (optional) to that folder. +3. Go to **Back Office > Modules > Module Manager**. +4. Search for "MySQL Memory Cache Optimized". +5. Click **Install**. + +## Technical Limitations + +* **Volatility:** Data stored in `MEMORY` tables is lost if the MySQL server restarts. Do not use this for persistent data. +* **Size Limit:** The maximum value size is approximately **16,000 characters** (due to `VARCHAR` limits in MEMORY tables and `utf8mb4` encoding). If you attempt to cache data larger than this, the `set()` method will return `false` and the data will not be cached. + +## Usage Examples + +You can call this module from any other module, controller, or hook in PrestaShop. + +### 1. Basic Usage (Get & Set) + +```php +// 1. Get the module instance +$cache = Module::getInstanceByName('dbmemorycache'); + +// Ensure module is installed and active +if ($cache && Module::isInstalled('dbmemorycache')) { + $key = 'my_custom_api_data_v1'; + + // 2. Check if data exists and is valid (not expired) + if ($cache->existsValue($key)) { + $data = $cache->getValue($key); + // $data is now your array/object + } else { + // 3. Fetch your expensive data + $data = [ + 'id' => 123, + 'name' => 'Product Name', + 'features' => ['A', 'B', 'C'] + ]; + + // 4. Store in cache for 1 hour (3600 seconds) + // Returns true on success, false if data is too big + $cache->setValue($key, $data, 3600); + } +} \ No newline at end of file diff --git a/config_uk.xml b/config_uk.xml new file mode 100644 index 0000000..9939b73 --- /dev/null +++ b/config_uk.xml @@ -0,0 +1,11 @@ + + + dbmemorycache + + + + + + 0 + 0 + \ No newline at end of file diff --git a/dbmemorycache.php b/dbmemorycache.php new file mode 100644 index 0000000..f1f9724 --- /dev/null +++ b/dbmemorycache.php @@ -0,0 +1,214 @@ +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}`"); + } +}