time(), 'data' => $data ]; // Store in memory cache self::$memoryCache[$key] = $cached; // Store in file cache $filePath = self::getCacheDir() . '/' . $key . '.json'; $written = @file_put_contents($filePath, json_encode($cached), LOCK_EX) !== false; if ($written) { // 0600: cache may feed security-relevant reads; keep it non-readable // to other local users and non-poisonable by pre-created files. @chmod($filePath, 0600); } return $written; } /** * Delete cached data * * @param string $prefix Cache prefix * @param mixed $identifier Unique identifier (null to delete all with prefix) * @return bool Success */ public static function delete(string $prefix, $identifier = null): bool { if ($identifier !== null) { $key = self::makeKey($prefix, $identifier); unset(self::$memoryCache[$key]); $filePath = self::getCacheDir() . '/' . $key . '.json'; return !file_exists($filePath) || @unlink($filePath); } // Delete all entries for this prefix. A key is either the bare prefix or // prefix + '_' + md5(identifier) (32 hex chars, see makeKey). Match exactly // that so a prefix can't clobber a different prefix that merely shares a // leading substring — e.g. delete('workflow') must not wipe 'workflow_rules'. $safePrefix = preg_replace('/[^a-zA-Z0-9_]/', '_', $prefix); $keyRegex = '/^' . preg_quote($safePrefix, '/') . '(_[0-9a-f]{32})?$/'; $files = glob(self::getCacheDir() . '/' . $safePrefix . '*.json') ?: []; foreach ($files as $file) { if (preg_match($keyRegex, basename($file, '.json'))) { @unlink($file); } } // Clear matching memory cache entries foreach (array_keys(self::$memoryCache) as $key) { if (preg_match($keyRegex, $key)) { unset(self::$memoryCache[$key]); } } return true; } /** * Clear all cache * * @return bool Success */ public static function clearAll(): bool { self::$memoryCache = []; $files = glob(self::getCacheDir() . '/*.json'); foreach ($files as $file) { @unlink($file); } return true; } /** * Get data from cache or fetch it using a callback * * @param string $prefix Cache prefix * @param mixed $identifier Unique identifier * @param callable $callback Function to call if cache miss * @param int $ttl Time-to-live in seconds * @return mixed Cached or freshly fetched data */ public static function remember(string $prefix, $identifier, callable $callback, int $ttl = 300) { $data = self::get($prefix, $identifier, $ttl); if ($data === null) { $data = $callback(); if ($data !== null) { self::set($prefix, $identifier, $data); } } return $data; } /** * Clean up expired cache files (call periodically) * * @param int $maxAge Maximum age in seconds (default 1 hour) */ public static function cleanup(int $maxAge = 3600): void { $files = glob(self::getCacheDir() . '/*.json'); $now = time(); foreach ($files as $file) { if ($now - filemtime($file) > $maxAge) { @unlink($file); } } } }