145 lines
3.8 KiB
PHP
145 lines
3.8 KiB
PHP
<?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);
|
|
$db = (int)Env::get('REDIS_DB', 0);
|
|
|
|
// 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 ($password !== null && $password !== '' && strtolower($password) !== 'null') {
|
|
$redis->auth($password);
|
|
}
|
|
|
|
if ($db > 0) {
|
|
$redis->select($db);
|
|
}
|
|
|
|
self::$client = $redis;
|
|
} catch (\Exception $e) {
|
|
error_log('[Cache Error] Redis connection error: ' . $e->getMessage());
|
|
self::$client = null;
|
|
}
|
|
|
|
return self::$client;
|
|
}
|
|
|
|
/**
|
|
* Helper to get prefixed key
|
|
*/
|
|
private static function getPrefixedKey(string $key): string
|
|
{
|
|
return 'nabeh:' . $key;
|
|
}
|
|
|
|
/**
|
|
* Retrieve cache item by key.
|
|
*/
|
|
public static function get(string $key)
|
|
{
|
|
$client = self::getClient();
|
|
if (!$client) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$prefixedKey = self::getPrefixedKey($key);
|
|
$value = $client->get($prefixedKey);
|
|
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 {
|
|
$prefixedKey = self::getPrefixedKey($key);
|
|
return $client->setex($prefixedKey, $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 {
|
|
$prefixedKey = self::getPrefixedKey($key);
|
|
return $client->del($prefixedKey) > 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;
|
|
}
|
|
}
|