專案初期或中小型應用,引入 Redis/Memcached 可能過重。一個設計良好的檔案快取類,支援 TTL 過期、序列化儲存、快取狀態判斷(無快取/有效/過期),即可滿足 80% 場景需求。本文提供生產就緒的 `FileCache` 類別完整程式碼與使用範例。
<?php
/**
* 輕量級檔案快取類
* 功能:TTL 過期、serialize 序列化、原子寫入、狀態回傳(0無/1有效/2過期)
*/
class FileCache
{
private string $cacheDir;
private int $defaultTtl;
public function __construct(string $cacheDir = './cache', int $defaultTtl = 3600)
{
$this->cacheDir = rtrim($cacheDir, '/') . '/';
$this->defaultTtl = $defaultTtl;
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
}
/**
* 寫入快取
* @return bool 成功與否
*/
public function set(string $key, mixed $value, int $ttl = null): bool
{
$ttl = $ttl ?? $this->defaultTtl;
$file = $this->cacheDir . 'cache_' . md5($key) . '.dat';
$tmpFile = $file . '.tmp.' . bin2hex(random_bytes(4));
$data = [
'expire' => $ttl > 0 ? time() + $ttl : 0,
'value' => $value,
];
$serialized = serialize($data);
if (file_put_contents($tmpFile, $serialized, LOCK_EX) === false) {
@unlink($tmpFile);
return false;
}
if (!@rename($tmpFile, $file)) {
@unlink($tmpFile);
return false;
}
@chmod($file, 0644);
return true;
}
/**
* 讀取快取
* @return array ['cached' => 0|1|2, 'value' => mixed]
* cached: 0=無快取, 1=有效, 2=已過期
*/
public function get(string $key): array
{
$file = $this->cacheDir . 'cache_' . md5($key) . '.dat';
$result = ['cached' => 0, 'value' => null];
if (!file_exists($file)) {
return $result;
}
$content = @file_get_contents($file);
if ($content === false) {
return $result;
}
$data = @unserialize($content);
if ($data === false || !isset($data['expire'], $data['value'])) {
// 損壞的快取檔,刪除
@unlink($file);
return $result;
}
if ($data['expire'] > 0 && $data['expire'] < time()) {
$result['cached'] = 2; // 過期
// 可選:自動清理過期檔
// @unlink($file);
return $result;
}
$result['cached'] = 1;
$result['value'] = $data['value'];
return $result;
}
public function delete(string $key): bool
{
$file = $this->cacheDir . 'cache_' . md5($key) . '.dat';
return file_exists($file) ? unlink($file) : false;
}
public function clear(): bool
{
$files = glob($this->cacheDir . 'cache_*.dat');
$success = true;
foreach ($files as $f) {
if (!@unlink($f)) $success = false;
}
return $success;
}
// 便利方法:直接取得值或預設值
public function fetch(string $key, mixed $default = null): mixed
{
$res = $this->get($key);
return $res['cached'] === 1 ? $res['value'] : $default;
}
}
require_once 'FileCache.php';
$cache = new FileCache(__DIR__ . '/runtime/cache', 1800); // 30 分鐘預設 TTL
// 寫入
$cache->set('site_config', ['theme' => 'dark', 'lang' => 'zh-TW'], 3600);
// 讀取(判斷狀態)
$result = $cache->get('site_config');
switch ($result['cached']) {
case 1:
echo "快取命中: ";
print_r($result['value']);
break;
case 2:
echo "快取過期,重新查詢資料庫...";
// 重新查詢 DB -> $cache->set(...)
break;
case 0:
default:
echo "無快取,首次查詢資料庫...";
break;
}
// 簡易用法:直接取值或預設
$config = $cache->fetch('site_config', ['theme' => 'light', 'lang' => 'en']);
// 刪除
$cache->delete('site_config');
// 清空所有
$cache->clear();
| 設計點 | 做法 | 理由 |
|---|---|---|
| 檔案命名 | `cache_{md5(key)}.dat` | 避免鍵值特殊字元導致路徑問題;長度固定利於目錄掃描 |
| 原子寫入 | 寫入 `.tmp.xxxx` → `rename()` 覆蓋 | 避免高併發讀到半寫入檔案;`rename` 在同檔案系統下為原子操作 |
| 鎖定機制 | `file_put_contents(..., LOCK_EX)` | 防止多進程同時寫入同一暫存檔(極低機率衝突) |
| 資料結構 | 陣列包 `expire` + `value` | 單一檔案自包含過期資訊,無需額外索引檔 |
| 序列化 | `serialize()` / `unserialize()` | 支援任意 PHP 型別(陣列、物件、資源除外);可換 `igbinary` 提升效能 |
| 狀態碼 | 0/1/2 三態 | 呼叫端可精確得知狀態,決定是否回源查詢 |
// 在類別內新增
private array $tagIndex = [];
private string $tagIndexFile;
public function __construct(...) {
// ...
$this->tagIndexFile = $this->cacheDir . '.tags.json';
if (file_exists($this->tagIndexFile)) {
$this->tagIndex = json_decode(file_get_contents($this->tagIndexFile), true) ?? [];
}
}
public function setWithTags(string $key, mixed $value, array $tags, int $ttl = null): bool
{
if (!$this->set($key, $value, $ttl)) return false;
foreach ($tags as $tag) {
$this->tagIndex[$tag][$key] = true;
}
$this->saveTagIndex();
return true;
}
public function deleteByTag(string $tag): int
{
$keys = $this->tagIndex[$tag] ?? [];
$count = 0;
foreach (array_keys($keys) as $key) {
if ($this->delete($key)) $count++;
}
unset($this->tagIndex[$tag]);
$this->saveTagIndex();
return $count;
}
private function saveTagIndex(): void
{
file_put_contents($this->tagIndexFile, json_encode($this->tagIndex), LOCK_EX);
}
這個檔案快取類別零依賴、介面清晰、功能完備,適合直接納入專案共用庫,或作為 PSR-16 介面的簡易實作替代品。