HTTP キャッシュと ETag#
ページ単位で HTTP キャッシュポリシーを宣言できます。フレームワークが Cache-Control / Vary / ETag / Last-Modified を出力し、安全メソッド(GET/HEAD)の条件付きリクエストに 304 Not Modified を返します。
クラススタイル(#[Cache] 属性)#
use Polidog\Relayer\Http\Cache;
#[Cache(maxAge: 3600, public: true, etag: 'home-v1')]
final class HomePage extends PageComponent { /* ... */ }指定できる主なパラメータ:
| パラメータ | 意味 |
|---|---|
maxAge / sMaxAge | max-age / s-maxage(秒) |
public / private | キャッシュ共有可否 |
noStore / noCache | no-store / no-cache |
mustRevalidate / immutable | 再検証必須 / 不変 |
vary | Vary ヘッダ(配列) |
etag | 静的 ETag 値 |
etagKey | 動的 ETag のキー(EtagStore で解決) |
etagWeak | 弱い ETag(W/"…") |
lastModified | Last-Modified(strtotime() が解せる文字列) |
関数スタイルは属性の代わりに $ctx->cache():
return function (PageContext $ctx): Closure {
$ctx->cache(new Cache(maxAge: 60, public: true, etagKey: 'feed'));
return function () use ($ctx): \Polidog\UsePhp\Runtime\Element {
// 重い処理はここ。304 のときは実行されない
};
};ファクトリは毎リクエスト実行(軽量)、返したレンダークロージャは 304 でないときだけ実行されます。つまり 304 ショートサーキットはレンダー本体の重い処理を丸ごと節約します。
静的 ETag と、EtagStore による動的 ETag#
静的 ETag(etag)#
#[Cache(maxAge: 3600, public: true, etag: 'home-v1')]
final class HomePage extends PageComponent { /* ... */ }etag: 'home-v1' は「デプロイでしか変わらない」コンテンツ向き。
動的 ETag(etagKey + EtagStore)#
データ駆動ページは etagKey: を宣言し、EtagStore がリクエスト時に現在値を解決します。クライアントの If-None-Match が一致すれば、 ページもリポジトリのコードも一切走らず(DB に触れず)304 を返します。
#[Cache(maxAge: 60, public: true, etagKey: 'user-list')]
final class UsersPage extends PageComponent { /* ... */ }
etag(静的)とetagKey(動的)両方が指定された場合は静的etagが優先。
既定バックエンド: FileEtagStore#
標準で FileEtagStore が登録され、$projectRoot/var/cache/etags/ にキーごと(sha1(key) 1ファイル、write-then-rename のアトミック書き込み)で保存します。設定不要です。
データを更新する側が、変わったタイミングで ETag 値を更新します:
use Polidog\Relayer\Http\EtagStore;
final class UserRepository
{
public function __construct(private readonly EtagStore $etags) {}
public function save(User $user): void
{
// ... 永続化
$this->etags->set('user-list', \sha1((string) \microtime(true)));
}
}EtagStore のメソッド: get(string $key): ?string / set(string $key, string $etag): void / forget(string $key): void。
カスタムバックエンド(Redis 例)#
use Polidog\Relayer\Http\EtagStore;
final class RedisEtagStore implements EtagStore
{
public function __construct(
private readonly \Redis $redis,
private readonly string $prefix = 'etag:',
) {}
public function get(string $key): ?string
{
$v = $this->redis->get($this->prefix . $key);
return \is_string($v) && '' !== $v ? $v : null;
}
public function set(string $key, string $etag): void
{
$this->redis->set($this->prefix . $key, $etag);
}
public function forget(string $key): void
{
$this->redis->del($this->prefix . $key);
}
}config/services.yaml で差し替え:
services:
Polidog\Relayer\Http\EtagStore:
alias: App\Infrastructure\RedisEtagStore条件付き GET / 304 Not Modified#
etag か lastModified があると、安全メソッドで If-None-Match / If-Modified-Since を評価します。
- 検証ヘッダ(
ETag/Last-Modified/Cache-Control/Vary)を出力 - 一致すれば
304 Not Modifiedをセット - ボディ描画前にリクエストを終了
ETag 比較は RFC 7232 §2.3.2 の弱い比較に従い、W/"v1" と "v1" は一致、 * は任意のタグに一致します。
完全な例#
#[Cache(
maxAge: 3600,
public: true,
vary: ['Accept-Language'],
etag: 'home-v1',
etagWeak: true,
lastModified: '2025-01-15 10:00:00 UTC',
)]
final class HomePage extends PageComponent { /* ... */ }このサイトでの使い方#
本サイトは読み取り系(ホーム / 記事 / 検索 / /api/search)を 時間ベースの 2 ノブでキャッシュします。共有ポリシー App\PageCache::timed() が Cache-Control: public, max-age=300, s-maxage=2592000(ブラウザ 5 分 / CDN 30 日)+ 本文ハッシュの静的 etag を出します(etagKey/EtagStore は不使用=ETag 値そのものが本文ダイジェスト)。
ヒット中はオリジン(PHP / リモート Turso)に一切触れず、TTL 切れ後の再検証だけ content-addressed ETag で安く 304 に短絡します。よって bin/docs の編集は即時反映ではなく、ブラウザは最大 max-age、CDN は自然失効またはパージで反映されます(旧構成は no-cache + 本文ハッシュ ETag で即時反映を優先していましたが、リモート Turso 往復を毎回起こすため時間ベースへ変更)。CDN 配下での要点(s-maxage を効かせる Cache Rule、セッション Cookie で全 BYPASS する罠など)は CDN キャッシュ、デプロイ全般は デプロイ を参照。
変更履歴 (1)
- 新規作成