From 408b4b0048234751e97315c8c471063719bd48cf Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 23 May 2026 00:20:07 +0300 Subject: [PATCH] Deploy: 2026-05-23 00:20:07 --- backend/.env.example | 7 + backend/app/Core/Cache.php | 128 ++++++++++++++++++ backend/app/Models/CompanySubscription.php | 34 +++-- .../app/Models/CompanySubscriptionUsage.php | 29 +++- backend/public/test_redis_cache.php | 75 ++++++++++ 5 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 backend/app/Core/Cache.php create mode 100644 backend/public/test_redis_cache.php diff --git a/backend/.env.example b/backend/.env.example index 065294b..a07feb9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -27,3 +27,10 @@ HMAC_SALT=nabeh_secure_blind_index_salt_key_123 # Secret key for JWT signatures JWT_SECRET=nabeh_jwt_secret_signature_key_987654 +# Redis Settings (Optional caching - falls back to DB if disabled/unavailable) +REDIS_ENABLED=false +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD= + + diff --git a/backend/app/Core/Cache.php b/backend/app/Core/Cache.php new file mode 100644 index 0000000..fec9b3b --- /dev/null +++ b/backend/app/Core/Cache.php @@ -0,0 +1,128 @@ +connect($host, $port, 1.0); + if (!$connected) { + error_log("[Cache Warning] Failed to connect to Redis at {$host}:{$port}"); + return null; + } + + if (!empty($password)) { + $redis->auth($password); + } + + self::$client = $redis; + } catch (\Exception $e) { + error_log('[Cache Error] Redis connection error: ' . $e->getMessage()); + self::$client = null; + } + + return self::$client; + } + + /** + * Retrieve cache item by key. + */ + public static function get(string $key) + { + $client = self::getClient(); + if (!$client) { + return null; + } + + try { + $value = $client->get($key); + return $value !== false ? json_decode($value, true) : null; + } catch (\Exception $e) { + error_log('[Cache Error] Redis get failed: ' . $e->getMessage()); + return null; + } + } + + /** + * Set cache item by key with TTL. + */ + public static function set(string $key, $value, int $ttl = 3600): bool + { + $client = self::getClient(); + if (!$client) { + return false; + } + + try { + return $client->setex($key, $ttl, json_encode($value)); + } catch (\Exception $e) { + error_log('[Cache Error] Redis set failed: ' . $e->getMessage()); + return false; + } + } + + /** + * Delete cache item by key. + */ + public static function delete(string $key): bool + { + $client = self::getClient(); + if (!$client) { + return false; + } + + try { + return $client->del($key) > 0; + } catch (\Exception $e) { + error_log('[Cache Error] Redis delete failed: ' . $e->getMessage()); + return false; + } + } + + /** + * Retrieve cache item, or run callback and store results if cache misses. + */ + public static function remember(string $key, int $ttl, callable $callback) + { + $value = self::get($key); + if ($value !== null) { + return $value; + } + + $value = $callback(); + self::set($key, $value, $ttl); + return $value; + } +} diff --git a/backend/app/Models/CompanySubscription.php b/backend/app/Models/CompanySubscription.php index f79a6cc..9dede7d 100644 --- a/backend/app/Models/CompanySubscription.php +++ b/backend/app/Models/CompanySubscription.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Core\Database; +use App\Core\Cache; /** * CompanySubscription Model @@ -17,19 +18,23 @@ class CompanySubscription extends BaseModel */ public static function findActiveByCompany(int $companyId): ?array { - $now = date('Y-m-d H:i:s'); - return Database::selectOne( - "SELECT cs.*, sp.name as plan_name, sp.max_sessions, sp.max_requests, - sp.max_voice_requests, sp.max_ocr_requests, sp.features - FROM " . static::$table . " cs - JOIN subscription_plans sp ON cs.plan_id = sp.id - WHERE cs.company_id = ? - AND cs.status = 'active' - AND cs.starts_at <= ? - AND cs.ends_at >= ? - LIMIT 1", - [$companyId, $now, $now] - ); + $cacheKey = "company_subscription_{$companyId}"; + + return Cache::remember($cacheKey, 300, function () use ($companyId) { + $now = date('Y-m-d H:i:s'); + return Database::selectOne( + "SELECT cs.*, sp.name as plan_name, sp.max_sessions, sp.max_requests, + sp.max_voice_requests, sp.max_ocr_requests, sp.features + FROM " . static::$table . " cs + JOIN subscription_plans sp ON cs.plan_id = sp.id + WHERE cs.company_id = ? + AND cs.status = 'active' + AND cs.starts_at <= ? + AND cs.ends_at >= ? + LIMIT 1", + [$companyId, $now, $now] + ) ?: []; + }) ?: null; } /** @@ -47,6 +52,9 @@ class CompanySubscription extends BaseModel [$companyId] ); + // Clear active subscription cache for the company + Cache::delete("company_subscription_{$companyId}"); + return self::create([ 'company_id' => $companyId, 'plan_id' => $planId, diff --git a/backend/app/Models/CompanySubscriptionUsage.php b/backend/app/Models/CompanySubscriptionUsage.php index d805624..a4fc98e 100644 --- a/backend/app/Models/CompanySubscriptionUsage.php +++ b/backend/app/Models/CompanySubscriptionUsage.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Core\Database; +use App\Core\Cache; /** * CompanySubscriptionUsage Model @@ -20,7 +21,14 @@ class CompanySubscriptionUsage extends BaseModel $billingStart = date('Y-m-d', strtotime($activeSubscription['starts_at'])); $billingEnd = date('Y-m-d', strtotime($activeSubscription['ends_at'])); - // Check if usage record already exists + $cacheKey = "company_usage_{$companyId}_{$billingStart}_{$billingEnd}"; + + $cached = Cache::get($cacheKey); + if ($cached) { + return $cached; + } + + // Check if usage record already exists in database $usage = Database::selectOne( "SELECT * FROM " . static::$table . " WHERE company_id = ? AND billing_start = ? AND billing_end = ? @@ -39,7 +47,7 @@ class CompanySubscriptionUsage extends BaseModel 'voice_count' => 0, 'ocr_count' => 0 ]); - return [ + $usage = [ 'id' => $id, 'company_id' => $companyId, 'billing_start' => $billingStart, @@ -62,6 +70,9 @@ class CompanySubscriptionUsage extends BaseModel } } + // Cache usage data for 60 seconds (short TTL since usage changes frequently) + Cache::set($cacheKey, $usage, 60); + return $usage; } @@ -84,12 +95,24 @@ class CompanySubscriptionUsage extends BaseModel $column = 'ocr_count'; } - return Database::execute( + $success = Database::execute( "UPDATE " . static::$table . " SET {$column} = {$column} + ? WHERE id = ?", [$amount, $currentUsage['id']] ) > 0; + + if ($success) { + // Sync Redis cache with incremented values + $cacheKey = "company_usage_{$companyId}_{$currentUsage['billing_start']}_{$currentUsage['billing_end']}"; + $cached = Cache::get($cacheKey); + if ($cached) { + $cached[$column] += $amount; + Cache::set($cacheKey, $cached, 60); + } + } + + return $success; } /** diff --git a/backend/public/test_redis_cache.php b/backend/public/test_redis_cache.php new file mode 100644 index 0000000..b7fa64a --- /dev/null +++ b/backend/public/test_redis_cache.php @@ -0,0 +1,75 @@ +getProperty('connectionAttempted'); + $connAttempted->setAccessible(true); + $connAttempted->setValue(null, false); + + $clientProp = $ref->getProperty('client'); + $clientProp->setAccessible(true); + $clientProp->setValue(null, null); + + $res = Cache::set('test_cache_key', ['hello' => 'world'], 30); + if ($res) { + echo " ✅ Successfully set value in Redis.\n"; + $val = Cache::get('test_cache_key'); + if (isset($val['hello']) && $val['hello'] === 'world') { + echo " ✅ Successfully retrieved value from Redis: " . json_encode($val) . "\n"; + } else { + echo " ❌ Failed to get correct value from Redis.\n"; + } + Cache::delete('test_cache_key'); + } else { + echo " ℹ️ Redis server is not running or unreachable (Fallback triggered, test completed successfully).\n"; + } + } catch (\Exception $e) { + echo " Reflection or execution error: " . $e->getMessage() . "\n"; + } +} else { + echo "\n2. Redis extension (phpredis) not installed. Native database fallback remains active.\n"; +} + +echo "\n=== Redis Caching Tests Completed ===\n";