類別設計目標
- 輕量、無依賴、單檔案即用。
- 支援多實例、自定義快取目錄。
- 原子寫入 (flock LOCK_EX) 避免併發損壞。
- 內建過期機制 (分鐘精度)。
- 提供
clearAll() 一鍵清空。
完整代碼 (PSR-4 友善、型別提示)
<?php
namespace App\Cache;
class FileCache {
private string $directory;
private string $keySalt;
public function __construct(string $directory = '', string $keySalt = 'cache_salt_v1') {
$this->keySalt = $keySalt;
if ($directory) $this->setDirectory($directory);
}
public function setDirectory(string $path): void {
$this->directory = rtrim($path, '/') . '/';
$this->ensureDirectory($this->directory);
}
/** 讀取快取,未命中或過期回傳 false */
public function get(string $key, int $ttlMinutes = 1): mixed {
$file = $this->buildPath($key);
if (!is_file($file)) return false;
$data = @unserialize(file_get_contents($file));
if (!$data || !isset($data['time'], $data['value'])) return false;
if (time() - $data['time'] > $ttlMinutes * 60) {
@unlink($file); // 順手清理過期檔
return false;
}
return $data['value'];
}
/** 寫入快取,回傳 bool */
public function set(string $key, mixed $value): bool {
$file = $this->buildPath($key);
$fp = fopen($file, 'cb'); // 'c' 不截斷, 'b' 二進位
if (!$fp) return false;
if (flock($fp, LOCK_EX)) {
$payload = serialize(['value' => $value, 'time' => time()]);
$written = fwrite($fp, $payload);
ftruncate($fp, $written); // 確保無殘留
flock($fp, LOCK_UN);
fclose($fp);
return $written !== false;
}
fclose($fp);
return false;
}
/** 刪除單筆 */
public function delete(string $key): bool {
$file = $this->buildPath($key);
return is_file($file) && @unlink($file);
}
/** 清空整個快取目錄 */
public function clearAll(): void {
$this->rrmdir($this->directory);
$this->ensureDirectory($this->directory);
}
private function buildPath(string $key): string {
$hash = md5($key . $this->keySalt);
return $this->directory . $key . '_' . $hash . '.cache';
}
private function ensureDirectory(string $path): void {
if (!is_dir($path)) {
if (!mkdir($path, 0777, true) && !is_dir($path)) {
throw new \RuntimeException("無法建立快取目錄: $path");
}
}
}
private function rrmdir(string $dir): void {
if (!is_dir($dir)) return;
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
rmdir($dir);
}
}
// 使用範例
$cache = new FileCache(__DIR__ . '/storage/cache', 'my_app_v2');
$data = $cache->get('homepage_stats', 5); // 5 分鐘快取
if ($data === false) {
$data = ['pv' => 12345, 'uv' => 6789, 'updated' => date('c')];
$cache->set('homepage_stats', $data);
}
var_dump($data);
進階擴充建議
- 標籤/分組失效:維護
tag -> [keys] 映射檔,invalidateTag('user_123') 批量刪除相關鍵。
- 防止快取穿透:
getOrSet($key, $callable, $ttl) 單飛模式,併發時僅一個請求執行 $callable,其餘等待結果。
- 效能優化:高頻熱鍵改用 Redis/Memcached,本類適合中低頻、結構化、需持久化的資料 (設定檔、報表、第三方 API 回應)。
- 序列化替代:
igbinary_serialize (更小、更快) 或 json_encode (跨語言、可讀),需注意資料類型還原 (JSON 無資源型別)。
生產環境部署注意
- 快取目錄**不可**在 Web 公開根目錄下,防止直接下載
.cache 洩露敏感資料。
- 設定
open_basedir 限制 PHP 僅能讀寫指定目錄。
- 定期清理過期檔 (cron 執行
find /cache -name '*.cache' -mmin +1440 -delete) 或改用 tmpfs 掛載快取目錄 (重啟自動清空、極速 IO)。