PHPで「条件付きGET」を実装

PHPは、動的にコンテンツを生成するために開発された言語であり、常に最新の情報を反映する必要があるため、ブラウザがサーバからの出力を常に取得するようになっています。

ただし、「条件付きGET」という仕組みを使用することで、PHPなどの動的に出力されるコンテンツでもブラウザのキャッシュを使うことができます。条件付きGETは、ブラウザがリクエストを送信する際に、サーバに対してコンテンツが更新されているかどうかを尋ね、更新されていなければブラウザが持つキャッシュを利用させる仕組みです。これにより、サーバの負荷をゼロ近くにまで軽減し、ページの読み込み速度を飛躍的に向上させることができます。

## 書いたコード

```php
<?php
class CGET {
    public function __construct($timestamp = 0) {
        if (!$timestamp) {
            return;
        }
        $this->run($timestamp);
    }

    private function run($timestamp) {
        // ETagの生成
        $etag = sprintf('"%s"', md5($timestamp));
        $etagHeader = filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH');

        // 判定順序：ETag → Last-Modified（ETagの方が厳密なため）
        if ($etagHeader === $etag) {
            $this->switchBrowserCache();
            exit;
        }
        header('ETag: ' . $etag);

        // Last-Modifiedの生成
        $lastModified = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
        $ifModifiedSince = filter_input(INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE');

        if ($ifModifiedSince) {
            // If-Modified-Sinceヘッダーを日時に変換して比較
            $ifModifiedSinceTimestamp = strtotime($ifModifiedSince);
            if ($ifModifiedSinceTimestamp >= $timestamp) {
                $this->switchBrowserCache();
                exit;
            }
        }

        header('Last-Modified: ' . $lastModified);
    }

    private function switchBrowserCache() {
        // 304 Not Modifiedのステータスコードを返し、Content-Lengthを0に設定
        header('HTTP/1.1 304 Not Modified');
        header('Content-Length: 0');
    }
}
```

よく見かけるサンプルコードと基本的には同じです。

## 使い方

```php
new CGET(filemtime(__FILE__));
readfile(__FILE__);
```

上記のように書くだけで動作します。

ポイントがいくつかあります。

## ETagとは？

ETagは、コンテンツの同一性を識別するための仕組みです。サーバはコンテンツを生成するたびに、ETagと呼ばれる一意の識別子を付与し、ブラウザに返します。ブラウザは次回以降のリクエストに際して、If-None-Matchヘッダーを用いてサーバにETagを送信し、コンテンツが更新されていなければ、サーバはコンテンツを再度生成することなく、304 Not Modifiedレスポンスを返します。

一般的には、コンテンツを実際に生成して比較するのは負荷が大きくなるため、更新日時をETagの値として紐づけることが多くあります。この方法を採用することで、コンテンツが変更されたかどうかを迅速に判断することができます。

当記事でも、更新日時をETagの値として利用する簡易的な実装を採用しました。ただし、ETagは更新日時以外の値も使用することができます。例えば、コンテンツのハッシュ値やバージョン番号をETagに使用することもできます。

## それぞれのリクエストヘッダに含まれる値について

ブラウザがリクエストを送信する際には、`$_SERVER['HTTP_IF_MODIFIED_SINCE']` ヘッダーや `$_SERVER['HTTP_IF_NONE_MATCH']` ヘッダーなどの「検証子」が付与されます。これらのヘッダーには、以前にサーバから返されたETagの値や、最終更新日時などが含まれています。ブラウザは、次回以降のリクエストに際して、これらの検証子を使用して、サーバ側にキャッシュの有効性を問い合わせます。

サーバは、検証子に基づいてクライアントのキャッシュの有効性を判断し、以下のいずれかのレスポンスを返します。

1. 304 Not Modifiedレスポンス：サーバ側のコンテンツが変更されていない場合に返されます。この場合、サーバはクライアントに新しいコンテンツを送信する必要がないため、レスポンスボディは空のままです。
2. 200 OKレスポンス：サーバ側のコンテンツが変更されている場合に返されます。この場合、サーバは新しいコンテンツをレスポンスボディに含めてクライアントに送信します。

これらの検証子を使用することで、ブラウザがキャッシュしたコンテンツを再利用しながらも、必要に応じて最新のコンテンツを取得することができます。

## ETag判定だけでもよい

条件付きGETのサンプルコードでは、一般的にETagヘッダとLast-Modifiedヘッダの両方の判定を行うことがありますが、ETagヘッダだけで条件付きGETを実現することも可能です。

```php
function cget($timestamp = 0) {
    if (!$timestamp) {
        return;
    }

    // ETagの生成
    $etag = sprintf('"%s"', md5($timestamp));
    $etagHeader = filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH');

    if ($etagHeader !== $etag) {
        header('ETag: ' . $etag);
        return;
    }

    // ETagが一致する場合は304を返して終了
    header('HTTP/1.1 304 Not Modified');
    header('Content-Length: 0');
    exit;
}
```

ただし、ETagの生成ロジックが信頼できるものでなければ、ETagの方が緩い判定となってしまうこともあります。たとえば、内容に基づいたETagを生成していない場合、変更を検知できない可能性があります。そのため、可能であれば`md5_file()`や`sha1_file()`などで内容に基づくETagを生成することが望ましいです。

動的コンテンツを扱う場合、セッション情報やユーザーの状態によってページの内容が変わるケースがあります。その際は、ユーザー単位にETagを生成する、もしくはETagを使わない判断も重要になります。

## Last-Modifiedは本当に不要？

過去には、「更新日時情報はSEO対策として重要」という都市伝説が広がり、Last-Modifiedヘッダーが有効であると考えられていました。しかし、Googleは更新日時情報を扱う際には非常に賢く、作為的な更新はむしろ逆効果になる可能性があります。

Last-Modifiedは依然として互換性の高い手段であり、リソースの内容更新が明確に時刻と紐づく場合には有効です。ただし、動的なコンテンツや環境に依存する表示の場合にはETagの方が適しているケースが多いです。

## ETagの値はクオートで囲む

ETagヘッダーの値は、HTTPの仕様（RFC 2616）によって、必ずダブルクオートで囲む必要があります。これは、値の比較時の一貫性と正確性を保つためです。

```
ETag: "xyzzy"
ETag: W/"xyzzy"
ETag: ""
```

また、`W/` というプレフィックスを使うことで「弱いETag」としてマークすることができます（※後述）。弱いETagは、バイトレベルで完全一致していなくても「同一」と見なすために使われます。

## フロー図で理解する条件付きGET

```mermaid
flowchart TD
  A[ブラウザからリクエスト] --> B{If-None-Matchあり？}
  B -- 一致 --> C[304 Not Modified]
  B -- 不一致 --> D{If-Modified-Sinceあり？}
  D -- 新しくない --> C
  D -- 更新あり --> E[200 OK + 新しいレスポンス]
  B -- ヘッダーなし --> E
```

## その他の考慮点

### 強い／弱いETagの使い分け

強いETag（例: "abc123"）はバイト単位で完全一致する場合にのみ有効です。静的ファイルや完全に同じ内容を配信するAPIレスポンスなどではこちらが適しています。一方、弱いETag（例: W/"abc123"）は表示上の変化がない程度の更新（HTMLの整形のみの違いなど）であれば同一と見なせるため、差分を緩く扱いたい場面に向いています。

### CDNやプロキシとの相性

一部のCDNやリバースプロキシ（例: Varnish, Cloudflare）はETagヘッダーを削除・変更することがあります。そういった環境では、Last-Modifiedの併用を行う設計を検討するとよいでしょう。

### モバイルや言語設定による出し分け

同じURLでもユーザーの環境（モバイル／デスクトップ、言語設定など）によって内容が変わる場合、ETagはそれらのバリエーションを識別する必要があります。たとえば `md5($content . $userAgent . $language)` のようにETag生成時に考慮するとよいでしょう。

## 補足資料

* [https://developer.mozilla.org/ja/docs/Web/HTTP/Conditional\_requests](https://developer.mozilla.org/ja/docs/Web/HTTP/Conditional_requests)
* [https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19)