<?php
declare(strict_types=1);
class FileCache implements \Psr\SimpleCache\CacheInterface {
private string $directory;
private int $defaultTtl;
public function __construct(string $directory, int $defaultTtl = 3600) {
$this->directory = rtrim($directory, '/');
$this->defaultTtl = $defaultTtl;
if (!is_dir($this->directory)) {
mkdir($this->directory, 0750, true);
}
}
private function getFilePath(string $key): string {
// Key 安全化:僅允許字母數字底線,避免目錄穿越
$safeKey = preg_replace('/[^a-zA-Z0-9_]/', '_', $key);
return $this->directory . '/' . $safeKey . '.cache.php';
}
private function serializeData(mixed $data, int $ttl): string {
$payload = [
'data' => $data,
'expire' => $ttl > 0 ? time() + $ttl : 0, // 0 = 永久
'created' => time(),
];
// 使用 var_export 產生合法 PHP 代碼,效能優于 serialize/igbinary
$code = '<?php return ' . var_export($payload, true) . '; ?>';
return $code;
}
public function get(string $key, mixed $default = null): mixed {
$file = $this->getFilePath($key);
if (!is_file($file)) return $default;
$payload = include $file; // 直接 include 回傳陣列,極快
if (!is_array($payload) || !isset($payload['data'], $payload['expire'])) {
$this->delete($key); // 損壞檔案自動清理
return $default;
}
if ($payload['expire'] > 0 && time() > $payload['expire']) {
$this->delete($key);
return $default;
}
return $payload['data'];
}
public function set(string $key, mixed $value, int $ttl = null): bool {
$ttl = $ttl ?? $this->defaultTtl;
$file = $this->getFilePath($key);
$tmpFile = $file . '.tmp.' . random_bytes(8); // 隨機暫存檔名
$content = $this->serializeData($value, $ttl);
// 原子寫入:寫暫存檔 → rename (同檔案系統保證原子性)
if (file_put_contents($tmpFile, $content) === false) return false;
if (!@rename($tmpFile, $file)) {
@unlink($tmpFile);
return false;
}
@chmod($file, 0640);
return true;
}
public function delete(string $key): bool {
$file = $this->getFilePath($key);
return is_file($file) && @unlink($file);
}
public function clear(): bool {
$files = glob($this->directory . '/*.cache.php');
$ok = true;
foreach ($files as $f) { if (!@unlink($f)) $ok = false; }
return $ok;
}
// PSR-16 其他介面方法 (getMultiple, setMultiple, deleteMultiple, has, hasMultiple) 略...
}
?>
| 特性 | 實作方式 | 效益 |
|---|---|---|
| 原子寫入 | 暫存檔 + `rename()` | 並發讀寫不會讀到損壞內容 |
| 安全 Key | `preg_replace('/[^a-zA-Z0-9_]/', '_', $key)` | 防目錄穿越 (`../`)、注入 |
| 高效序列化 | `var_export` + `include` | OPcache 可緩存編譯後 opcode,比 `unserialize` 快 2-5 倍 |
| 自動過期 | 讀取時檢查 `expire` 時間戳 | 無需背景 GC,懶惰清理 |
| 防禦性讀取 | `include` 失敗/格式錯誤自動 `delete` | 自癒,避免髒資料殘留 |
<?php
$cache = new FileCache('/var/www/cache', 1800);
// 寫入
$cache->set('user:123', ['name' => 'John', 'roles' => ['admin']], 3600);
// 讀取
$user = $cache->get('user:123'); // 陣列或 null
// 刪除
$cache->delete('user:123');
// 批次 (PSR-16)
$cache->setMultiple(['a'=>1, 'b'=>2]);
$vals = $cache->getMultiple(['a','b']); // ['a'=>1, 'b'=>2]
?>
檔案快取適合中小型專案、單機部署、開發環境。核心在於 **原子寫入 (rename)、安全 Key、高效序列化 (var_export+include)、懶惰過期清理**。實作 PSR-16 介面可無縫升級至 Redis/Memcached,架構更彈性。