`ob_start()` 開啟緩衝區 → PHP 執行產生 HTML → `ob_get_contents()` 取得內容 → 存檔 → 後續請求直接讀檔輸出,跳過 PHP 執行。
<?php
function pageCache(int $ttl = 300): void {
// 僅快取 GET 請求,POST 通常有副作用
if ($_SERVER['REQUEST_METHOD'] !== 'GET') return;
$cacheDir = __DIR__ . '/../var/cache/pages';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0750, true);
// Key 包含路徑 + 查詢字串 (排除追蹤參數 utm_*)
$query = $_SERVER['QUERY_STRING'];
parse_str($query, $params);
unset($params['utm_source'], $params['utm_medium'], $params['utm_campaign'], $params['fbclid'], $params['gclid']);
$key = $_SERVER['REQUEST_URI'] . '?' . http_build_query($params);
$key = 'page_' . hash('sha256', $key) . '.php';
$file = $cacheDir . '/' . $key;
// 檢查快取是否存在且未過期
if (is_file($file) && (time() - filemtime($file) < $ttl)) {
// 直接輸出快取內容 (已含 )
include $file;
exit;
}
// 註冊關閉函式:請求結束時寫入快取
register_shutdown_function(function () use ($file) {
$content = ob_get_contents();
ob_end_clean();
// 快取檔案格式: + HTML 內容
$cacheContent = "<?php exit; ?>" . $content;
$tmp = $file . '.tmp.' . random_bytes(4);
if (file_put_contents($tmp, $cacheContent) !== false) {
@rename($tmp, $file);
@chmod($file, 0640);
}
});
// 開啟緩衝區
ob_start();
}
// 入口檔案 (index.php / front controller) 最頂端呼叫
pageCache(300); // 5 分鐘快取
?>
<?php exit; ?><!DOCTYPE html>
<html><head><title>Page Title</title></head>
<body>...靜態 HTML 內容...</body></html>
直接 `include` 會因 `exit;` 終止執行,輸出後續 HTML,極快 (OPcache 緩存編譯後 opcode)。
| 坑點 | 後果 | 解決 |
|---|---|---|
| 快取了動態內容 (用戶名、購物車、CSRF Token) | 用戶 A 看到用戶 B 的資料、表單失效 | **僅快取純公開頁面** (首頁、列表、文章詳情)。動態區塊用 AJAX/ESI/SSI 片段快取 |
| POST 請求被快取 | 重複提交、副作用重複執行 | 強制 `REQUEST_METHOD === 'GET'` |
| 查詢字串導致快取爆炸 | 每個 utm_* 參數產生新快取 | 正規化 Key:移除追蹤參數、排序參數 |
| 多伺服器快取不一致 | 用戶輪詢到不同機器見到舊資料 | 共享儲存 (NFS/Redis) 或主動清除 (部署時 `clear()`) |
| 權限問題 | 無法寫入/讀取快取目錄 | 目錄擁有者 www-data,權限 750/640 |
PHP `ob_start` 全頁快取適合**中小型、單機、純公開內容**場景。核心在於 **Key 正規化、原子寫入、排除動態內容、部署時清除**。生產環境**強烈建議升級至 Nginx FastCGI Cache 或 CDN 邊緣快取**,架構更清晰、效能更極致。