PHPのHTTPクライアントであるGuzzleにおいて、外部エンドポイントへの重複通信を抑制しアプリケーションの応答速度を向上させるため、レスポンスキャッシュ機能を実装する中間層の構築プロセスを詳述する。本稿では、PHP-FIGが定めたPSR-6規格を基盤としたストレージ連携方法と、リクエスト特性を反映したキー生成アルゴリズムの設計方針を技術的観点から解説する。
PSR-6 インターフェースの統合
PSR-6はPHPエコシステムにおけるキャッシュ操作の標準化されたインターフェースであり、Psr\Cache\CacheItemPoolInterface と Psr\Cache\CacheItemInterface から構成される。Guzzleのコアライブラリ自体はキャッシュ機構を内包していないため、外部のキャッシュライブラリ(Memcached、Redis、APCu等)をPSR-6アダプタ経由で接続することで、保存先の実装に依存しない柔軟なアーキテクチャを構築できる。
ミドルウェアのスケルトン作成
Guzzleにおけるミドルウェアは、現在のハンドラを引数に取り、処理を追加した新しいハンドラを返却する高階関数として定義される。基本の制御フローは以下の構造となる。
use Psr\Http\Message\RequestInterface;
function buildCacheLayer($psr6Pool): callable {
return function (callable $nextHandler) use ($psr6Pool) {
return function (RequestInterface $req, array $config) use ($nextHandler, $psr6Pool) {
// キャッシュ判定ロジックおよびネットワーク転送処理
return $nextHandler($req, $config);
};
};
}
生成したクロージャは HandlerStack クラスの push() メソッドに登録し、パイプラインの任意の位置に挿入する。
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
$pipeline = HandlerStack::create();
$pipeline->push(buildCacheLayer($psr6Pool), 'cached_response');
$httpClient = new Client(['handler' => $pipeline]);
キャッシュキーの設計方針
キャッシュの有効性とストレージ効率は、キーの一意性と衝突回避戦略によって決定される。単なるパス情報だけでなく、HTTPメソッドや特定ヘッダーの値を複合させることで、同一リソースに対する多様なアクセスパターンを正確に分離する。
1. 基礎的なキー生成
通信メソッドとURIパスを組み合わせてハッシュ値を算出する方法が実装コストが最も低い。
$storageKey = hash('sha256', $req->getMethod() . $req->getUri()->getPath());
2. ヘッダー依存型キー生成
Accept や認証情報などでレスポンスペイロードが変化する場合は、ソート済みのヘッダー配列を結合させる。
$normalizedHeaders = $req->getHeaders();
ksort($normalizedHeaders);
$storageKey = hash('sha256', $req->getMethod() . $req->getUri() . serialize($normalizedHeaders));
3. HTTPキャッシュ制御指令の尊重
クライアント側が明示的にキャッシュ抑制を指示している場合、中間層の処理をバイパスし直接ネットワークへ問い合わせる。
$cacheDirective = $req->getHeaderLine('Cache-Control');
if (strpos($cacheDirective, 'no-store') !== false || strpos($cacheDirective, 'no-cache') !== false) {
return $nextHandler($req, $config);
}
統合実装例
上記の設計要素を結合した、PSR-6対応の完全なミドルウェア実装を示す。有効期間(TTL)はファクトリ関数の引数として分離し、デフォルト値を適用する。
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Promise\PromiseInterface;
function createCachedHandler(CacheItemPoolInterface $pool, int $lifetime = 3600): callable {
return function (callable $handler) use ($pool, $lifetime) {
return function (RequestInterface $req, array $opts) use ($handler, $pool, $lifetime) {
// キャッシュキーの算出処理
$keyValue = hash('sha256', $req->getMethod() . $req->getUri()->__toString());
// 既存エントリーの検証
$entry = $pool->getItem($keyValue);
if ($entry->isHit()) {
return \GuzzleHttp\Promise\resolve($entry->get());
}
// 未命中時は非同期HTTPリクエストを実行し、返却値を保存
return $handler($req, $opts)->then(
function (ResponseInterface $res) use ($entry, $pool, $lifetime) {
$entry->set($res)->expiresAfter($lifetime);
$pool->save($entry);
return $res;
}
);
};
};
}
運用時の考慮点
- 有効期間の動的算出: 静的なTTLではなく、レスポンスヘッダーの
Cache-Control: max-ageをパースし、サーバーから返却された期限を優先的に適用する。 - 条件付き検証の活用:
ETagやLast-Modifiedを保持し、再リクエスト時にIf-None-Matchヘッダーを付与して304 Not Modifiedを活用することで、ペイロード転送量を削減する。 - コンテンツ形式の分離: JSON APIデータと動的HTMLページのキャッシュ寿命や保存戦略を明確に区別し、メモリ領域の断片化を防ぐ。
- メトリクス収集: キャッシュ命中率やストレージ使用量をリアルタイムで監視し、キー設計の最適化フィードバックループを構築する。
Guzzle標準のミドルウェア実装を参照することで、内部プロミスハンドリングやエラー伝播のベストプラクティスを理解し、より堅牢なネットワークレイヤーを構築できる。