first commit
This commit is contained in:
62
README.md
Normal file
62
README.md
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
config_uk.xml
Normal file
11
config_uk.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<module>
|
||||||
|
<name>dbmemorycache</name>
|
||||||
|
<displayName><![CDATA[MySQL Memory Cache Pro]]></displayName>
|
||||||
|
<version><![CDATA[1.2.0]]></version>
|
||||||
|
<description><![CDATA[High-speed RAM cache. Auto-recovers after reboot.]]></description>
|
||||||
|
<author><![CDATA[YourName]]></author>
|
||||||
|
<tab><![CDATA[administration]]></tab>
|
||||||
|
<is_configurable>0</is_configurable>
|
||||||
|
<need_instance>0</need_instance>
|
||||||
|
</module>
|
||||||
214
dbmemorycache.php
Normal file
214
dbmemorycache.php
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB Memory Cache Module for PrestaShop
|
||||||
|
* Robust edition: Handles Reboots, Table Full errors, and Long Keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (!defined('_PS_VERSION_')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DbMemoryCache extends Module
|
||||||
|
{
|
||||||
|
private string $tableName;
|
||||||
|
|
||||||
|
// Static flag to ensure we only check table existence once per page load
|
||||||
|
private static bool $isInitialized = false;
|
||||||
|
|
||||||
|
// Max chars for value (approx 16KB safety limit for utf8mb4)
|
||||||
|
const MAX_CACHE_SIZE_CHARS = 16000;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user