All checks were successful
Publish Release / release (push) Successful in 10s
- Auto-create 'Initial Operating System' and 'Initial SSH Key' custom fields via Database::ensureCustomFields() on module load, eliminating the manual modify.sql step - Delete modify.sql (no longer needed) - Add try/catch blocks around every DB operation and API call across all PHP files per CLAUDE.md error handling rules - Add comprehensive PHPDoc to all classes, methods, and properties - Set up Laravel Pint (laravel/pint) with Laravel-style preset for consistent code formatting across the codebase - Add git pre-commit hook (hooks/pre-commit) that runs Pint on staged PHP files, auto-installed via Composer post-install/post-update scripts - Simplify README installation to a single copy-paste command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
185 lines
4.2 KiB
PHP
185 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace WHMCS\Module\Server\VirtFusionDirect;
|
|
|
|
/**
|
|
* Two-tier cache: uses Redis when the ext-redis extension is available, with an atomic
|
|
* filesystem fallback stored in the system temp directory.
|
|
*/
|
|
class Cache
|
|
{
|
|
const PREFIX = 'vfd:';
|
|
|
|
/** @var \Redis|null */
|
|
private static $redis = null;
|
|
|
|
/** @var bool|null */
|
|
private static $redisAvailable = null;
|
|
|
|
/** @var string */
|
|
private static $fileDir = '';
|
|
|
|
/**
|
|
* Try to connect to Redis. Returns the connection or null.
|
|
*/
|
|
private static function redis(): ?\Redis
|
|
{
|
|
if (self::$redisAvailable === false) {
|
|
return null;
|
|
}
|
|
|
|
if (self::$redis !== null) {
|
|
return self::$redis;
|
|
}
|
|
|
|
if (! extension_loaded('redis')) {
|
|
self::$redisAvailable = false;
|
|
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$redis = new \Redis;
|
|
$redis->connect('127.0.0.1', 6379, 1.0);
|
|
self::$redis = $redis;
|
|
self::$redisAvailable = true;
|
|
|
|
return $redis;
|
|
} catch (\Exception $e) {
|
|
self::$redisAvailable = false;
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the filesystem cache directory, creating it if needed.
|
|
*/
|
|
private static function fileDir(): string
|
|
{
|
|
if (self::$fileDir !== '') {
|
|
return self::$fileDir;
|
|
}
|
|
|
|
$dir = sys_get_temp_dir() . '/vfd_cache';
|
|
if (! is_dir($dir)) {
|
|
@mkdir($dir, 0700, true);
|
|
}
|
|
|
|
self::$fileDir = $dir;
|
|
|
|
return $dir;
|
|
}
|
|
|
|
/**
|
|
* Convert a cache key to a safe filename.
|
|
*/
|
|
private static function filePath(string $key): string
|
|
{
|
|
return self::fileDir() . '/' . md5($key) . '.cache';
|
|
}
|
|
|
|
/**
|
|
* Get a cached value.
|
|
*
|
|
* @param string $key
|
|
* @return mixed|null Returns null on miss
|
|
*/
|
|
public static function get($key)
|
|
{
|
|
// Try Redis first
|
|
$redis = self::redis();
|
|
if ($redis) {
|
|
try {
|
|
$data = $redis->get(self::PREFIX . $key);
|
|
if ($data !== false) {
|
|
return json_decode($data, true);
|
|
}
|
|
|
|
return null;
|
|
} catch (\Exception $e) {
|
|
// Fall through to file cache
|
|
}
|
|
}
|
|
|
|
// File cache fallback
|
|
$path = self::filePath($key);
|
|
if (! file_exists($path)) {
|
|
return null;
|
|
}
|
|
|
|
$raw = @file_get_contents($path);
|
|
if ($raw === false) {
|
|
return null;
|
|
}
|
|
|
|
$entry = json_decode($raw, true);
|
|
if (! $entry || ! isset($entry['expires']) || ! isset($entry['data'])) {
|
|
@unlink($path);
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($entry['expires'] < time()) {
|
|
@unlink($path);
|
|
|
|
return null;
|
|
}
|
|
|
|
return $entry['data'];
|
|
}
|
|
|
|
/**
|
|
* Store a value in cache.
|
|
*
|
|
* @param string $key
|
|
* @param mixed $value
|
|
* @param int $ttl Time-to-live in seconds
|
|
*/
|
|
public static function set($key, $value, $ttl = 300)
|
|
{
|
|
// Try Redis first
|
|
$redis = self::redis();
|
|
if ($redis) {
|
|
try {
|
|
$redis->setex(self::PREFIX . $key, $ttl, json_encode($value));
|
|
|
|
return;
|
|
} catch (\Exception $e) {
|
|
// Fall through to file cache
|
|
}
|
|
}
|
|
|
|
// File cache fallback with atomic write (race condition safe)
|
|
$path = self::filePath($key);
|
|
$tmp = $path . '.' . getmypid() . '.tmp';
|
|
$entry = json_encode(['expires' => time() + $ttl, 'data' => $value]);
|
|
|
|
if (@file_put_contents($tmp, $entry, LOCK_EX) !== false) {
|
|
@rename($tmp, $path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a cached value.
|
|
*
|
|
* @param string $key
|
|
*/
|
|
public static function forget($key)
|
|
{
|
|
$redis = self::redis();
|
|
if ($redis) {
|
|
try {
|
|
$redis->del(self::PREFIX . $key);
|
|
} catch (\Exception $e) {
|
|
// Continue to file cleanup
|
|
}
|
|
}
|
|
|
|
$path = self::filePath($key);
|
|
if (file_exists($path)) {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
}
|