RRelayer
ホーム/機能

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 / sMaxAgemax-age / s-maxage(秒)
public / privateキャッシュ共有可否
noStore / noCacheno-store / no-cache
mustRevalidate / immutable再検証必須 / 不変
varyVary ヘッダ(配列)
etag静的 ETag 値
etagKey動的 ETag のキー(EtagStore で解決)
etagWeak弱い ETag(W/"…"
lastModifiedLast-Modifiedstrtotime() が解せる文字列)

関数スタイルは属性の代わりに $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#

etaglastModified があると、安全メソッドで If-None-Match / If-Modified-Since を評価します。

  1. 検証ヘッダ(ETag / Last-Modified / Cache-Control / Vary)を出力
  2. 一致すれば 304 Not Modified をセット
  3. ボディ描画前にリクエストを終了

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 キャッシュ、デプロイ全般は デプロイ を参照。

最終更新: 2026-05-18
変更履歴 (1)
  • 新規作成