Deploy: 2026-05-23 00:20:07

This commit is contained in:
Hamza-Ayed
2026-05-23 00:20:07 +03:00
parent d8b5ddd6ff
commit 408b4b0048
5 changed files with 257 additions and 16 deletions

128
backend/app/Core/Cache.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
namespace App\Core;
/**
* Cache helper class providing Redis-based caching with automatic database fallback.
*/
class Cache
{
private static ?\Redis $client = null;
private static bool $connectionAttempted = false;
/**
* Get initialized Redis client or null if unavailable/disabled.
*/
private static function getClient(): ?\Redis
{
if (self::$connectionAttempted) {
return self::$client;
}
self::$connectionAttempted = true;
if (!Env::get('REDIS_ENABLED', false)) {
return null;
}
if (!class_exists('\Redis')) {
error_log('[Cache Warning] phpredis extension is not installed.');
return null;
}
try {
$redis = new \Redis();
$host = Env::get('REDIS_HOST', '127.0.0.1');
$port = (int)Env::get('REDIS_PORT', 6379);
$password = Env::get('REDIS_PASSWORD', null);
// Connect with 1.0s timeout to prevent request blocking
$connected = $redis->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;
}
}

View File

@@ -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,

View File

@@ -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;
}
/**