Files
dbmemorycache/dbmemorycache.php
2026-03-04 14:10:12 +02:00

215 lines
6.5 KiB
PHP

<?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}`");
}
}