檔案快取是中小型專案、無 Redis/Memcached 環境的首選。本文深度剖析三種主流實作:**魔術方法封裝**、**Key-Value 介面**、**單例模式進階版**,並補充鎖機制、序列化選擇、GC 清理策略,助你選出最適合的方案。
<?php
class FileCache {
public $ttl = 3600;
public $dir = __DIR__ . '/cache';
public function __construct() {
is_dir($this->dir) || mkdir($this->dir, 0755, true);
}
public function __set(string $key, mixed $val): void {
$this->set($key, $val);
}
public function __get(string $key): mixed {
return $this->get($key);
}
public function __isset(string $key): bool {
return $this->has($key);
}
public function __unset(string $key): void {
$this->delete($key);
}
protected function path(string $key): string {
return $this->dir . '/' . md5($key) . '.cache';
}
public function set(string $key, mixed $val, int $ttl = null): bool {
$data = ['v' => $val, 'e' => time() + ($ttl ?? $this->ttl)];
return file_put_contents($this->path($key), serialize($data), LOCK_EX) !== false;
}
public function get(string $key, mixed $default = null): mixed {
$file = $this->path($key);
if (!is_file($file)) return $default;
$data = @unserialize(file_get_contents($file));
if (!$data || time() > $data['e']) {
unlink($file);
return $default;
}
return $data['v'];
}
public function has(string $key): bool {
return $this->get($key, '__NOT_FOUND__') !== '__NOT_FOUND__';
}
public function delete(string $key): bool {
return @unlink($this->path($key));
}
public function clear(): void {
array_map('unlink', glob($this->dir . '/*.cache'));
}
}
// 使用
$cache = new FileCache();
$cache->user_1 = ['name' => 'Alice', 'vip' => true]; // __set
echo $cache->user_1['name']; // __get
unset($cache->user_1); // __unset
?>
✅ 語法糖豐富、適合簡單場景。⚠️ 魔術方法效能略低、難以型別提示。
<?php
class FileStore {
private string $path;
private int $defaultTtl;
public function __construct(string $path, int $defaultTtl = 3600) {
$this->path = rtrim($path, '/') . '/';
$this->defaultTtl = $defaultTtl;
is_dir($this->path) || mkdir($this->path, 0755, true);
}
private function file(string $key): string {
return $this->path . 'cache_' . md5($key) . '.php';
}
public function set(string $key, mixed $value, int $ttl = null): bool {
$ttl ??= $this->defaultTtl;
$content = "file($key), $content, LOCK_EX) !== false;
}
public function get(string $key, mixed $default = null): mixed {
$file = $this->file($key);
if (!is_file($file)) return $default;
$expire = (int)preg_replace('/^.*?EXPIRE:\s*(\d+).*$/s', '$1', file_get_contents($file));
if (time() > $expire) {
unlink($file);
return $default;
}
return include $file; // 直接執行 PHP 回傳值,極快
}
public function delete(string $key): bool { return @unlink($this->file($key)); }
public function flush(): void { array_map('unlink', glob($this->path . 'cache_*.php')); }
}
?>
✅ 使用 `var_export` + `include` 避免 `unserialize` 風險、速度極快、程式碼可讀。⚠️ 僅限 PHP 陣列/標量/物件(無資源)。
<?php
class Cache {
private static ?self $instance = null;
private string $dir;
private int $ttl;
private bool $locking;
private function __construct(string $dir, int $ttl = 3600, bool $locking = true) {
$this->dir = rtrim($dir, '/') . '/';
$this->ttl = $ttl;
$this->locking = $locking;
is_dir($this->dir) || mkdir($this->dir, 0755, true);
}
public static function init(string $dir, int $ttl = 3600, bool $locking = true): self {
return self::$instance ??= new self($dir, $ttl, $locking);
}
public static function getInstance(): self {
return self::$instance ?? throw new LogicException('Cache not initialized');
}
private function file(string $key): string {
return $this->dir . 'c_' . md5($key) . '.dat';
}
public function set(string $key, mixed $value, int $ttl = null, array $tags = []): bool {
$data = [
'v' => $value,
'e' => time() + ($ttl ?? $this->ttl),
't' => $tags, // 標籤
'm' => time(),
];
$fp = fopen($this->file($key), 'cb');
if (!$fp) return false;
if ($this->locking) flock($fp, LOCK_EX);
ftruncate($fp, 0);
fwrite($fp, serialize($data));
if ($this->locking) flock($fp, LOCK_UN);
fclose($fp);
// 寫入標籤索引
foreach ($tags as $tag) {
$this->addTagIndex($tag, $key);
}
return true;
}
private function addTagIndex(string $tag, string $key): void {
$idxFile = $this->dir . 'tag_' . md5($tag) . '.idx';
$keys = is_file($idxFile) ? unserialize(file_get_contents($idxFile)) : [];
$keys[$key] = true;
file_put_contents($idxFile, serialize($keys), LOCK_EX);
}
public function get(string $key, mixed $default = null): mixed {
$file = $this->file($key);
if (!is_file($file)) return $default;
$fp = fopen($file, 'rb');
if (!$fp) return $default;
if ($this->locking) flock($fp, LOCK_SH);
$data = unserialize(stream_get_contents($fp));
if ($this->locking) flock($fp, LOCK_UN);
fclose($fp);
if (!$data || time() > $data['e']) {
$this->delete($key);
return $default;
}
return $data['v'];
}
public function delete(string $key): bool {
// 從標籤索引移除
$file = $this->file($key);
if (is_file($file)) {
$data = @unserialize(file_get_contents($file));
if ($data && !empty($data['t'])) {
foreach ($data['t'] as $tag) $this->removeTagIndex($tag, $key);
}
}
return @unlink($file);
}
public function clearByTag(string $tag): int {
$idxFile = $this->dir . 'tag_' . md5($tag) . '.idx';
if (!is_file($idxFile)) return 0;
$keys = unserialize(file_get_contents($idxFile));
$count = 0;
foreach (array_keys($keys) as $key) {
if ($this->delete($key)) $count++;
}
unlink($idxFile);
return $count;
}
public function flush(): void {
array_map('unlink', glob($this->dir . '*'));
}
}
// 使用
Cache::init('/tmp/my_cache', 7200);
Cache::getInstance()->set('heavy_query', $rows, 3600, ['reports', 'dashboard']);
$rows = Cache::getInstance()->get('heavy_query');
Cache::getInstance()->clearByTag('reports'); // 一次清理相關快取
?>
| 格式 | 速度 | 體積 | 安全性 | 適用場景 |
|---|---|---|---|---|
| serialize/unserialize | 中 | 中 | ⚠️ 物件注入風險 | 需儲存物件、資源 |
| var_export + include | **快** | 大 | ✅ 純 PHP 程式碼 | 陣列/標量、高讀取頻率 |
| json_encode/decode | 中 | 小 | ✅ 安全 | 跨語言、API 快取 |
| igbinary (擴充套件) | **最快** | **最小** | ✅ 安全 | 高效能、大物件 |