RRelayer
ホーム/機能

HTTP クライアント#

外部 Web API を叩くための薄い契約。Database の「外部 API 版」で、ページ/コンポーネントが HttpClient 依存を取って直接呼びます。

設定不要で常に DI 登録されます(EtagStore と同じく registerDefaults 段で自動登録)。クライアントビルダー/ミドルウェアスタック/PSR-18 は意図的に持ちません(薄く保つ方針。Database がクエリビルダを持たないのと同じスタンス)。ext-curl が必須です(本番イメージの base は同梱済み)。

使い方#

引数に型で HttpClient を受け取るだけ。

<?php
use Polidog\Relayer\Http\Client\HttpClient;
use Polidog\Relayer\Router\Component\PageContext;

return function (PageContext $ctx, HttpClient $http): Closure {
    $res = $http->get('https://api.example.com/users/1');

    if (!$res->ok()) {
        $ctx->abort($res->status >= 500 ? 502 : 404);
    }

    $user = $res->json();
    return fn () => <h1>{$user['name']}</h1>;
};
  • get($url, $headers = []) — GET の近道。
  • request($method, $url, $headers = [], $body = null) — 汎用。

ヘッダは ['Authorization' => 'Bearer …'] のように name => value で渡します。

HttpResponse#

戻り値は readonlyHttpResponse(出力契約ではなく素の運び手なのでコンストラクタは開いています)。

メンバ/メソッド内容
$res->statusHTTP ステータスコード(int)
$res->headersレスポンスヘッダ array<string,string>
$res->body生ボディ(無いとき ''
$res->ok()2xx なら true
$res->json()ボディを JSON デコード(連想配列)。不正 JSON は HttpClientException
$res->header($name)ヘッダ 1 件をケース無視で取得(無ければ null

サーバ側がブラウザに返す Response とは別物です(こちらは外部 API が こちらに 返したもの)。

エラーの扱い#

4xx/5xx はエラーではありません。 status/headers/body が常に入った正常な HttpResponse として返るので、$res->status / $res->ok() で分岐します(0 行の SELECT が例外でないのと同じ)。

例外を投げるのは 転送失敗だけ(DNS/接続/TLS/タイムアウト/本文が来ない)。型は HttpClientException の 1 つだけで、元のドライバ例外は previous に保持されます。$res->json() に非 JSON を渡したときも同じ例外です。

use Polidog\Relayer\Http\Client\HttpClientException;

try {
    $res = $http->get($url);
} catch (HttpClientException $e) {
    // DNS/接続/TLS/タイムアウト等。レスポンスは存在しない
}

リクエストスコープキャッシュ#

1 ページが複数のコンポーネントから組み立てられる構成では、同じ外部エンドポイント(設定サービス、認証 API のプロフィール、…)に何度も当たることが珍しくありません。素朴に書けば 1 リクエストで同一 URL の往復が N 回発生するため、HttpClient のデフォルト実装は そのリクエストの間だけ レスポンスをメモ化します。Database の読み取りメモ化と同じ発想・同じ寿命です。

挙動の要点:

  • メモ化されるのは 安全メソッドだけGET / HEAD)。それ以外

POST / PUT / PATCH / DELETE / …)は毎回送信され、かつ送信前にキャッシュを 丸ごとフラッシュ します。「読む → 書く → また読む」で自分の書き込みが反映された結果を返すための、CachingDatabase と同じ単純で安全な挙動です(ホスト単位の精緻な無効化はしません)。

  • キャッシュキーは メソッド + URL + シリアライズしたヘッダ + ボディ

ヘッダや認証トークンが違えば別エントリです。

  • 寿命は 1 リクエスト。プロセス内配列でしかなく、永続化もリクエスト

間共有もしません。TTL も Cache-Control の解釈もなく、レスポンス側の freshness/revalidation は意図的にこの層で扱いません(より大きな問題なので別レイヤーで)。

  • 同じレスポンスが何度返ってきても HttpResponsereadonly なので

共有して安全です。$res->json() も同じインスタンスを再パースするだけ。

dev では CachingHttpClient が最外殻のデコレータなので、ヒットしたリクエストは TraceableHttpClient まで届きません。 プロファイラ のタイムラインでは実際の往復が http.request スパンとして並び、節約された方は http.cache_hit イベントのピンとして見えます。

例: 同じページの 2 つのコンポーネントが認証 API のプロフィールを引く。 URL もヘッダも同一なので、2 回目は往復ゼロ(http.cache_hit)。

// HeaderComponent.php
$me = $http->get('https://auth.example.com/me', ['Authorization' => 'Bearer ' . $token])->json();

// SidebarComponent.php — 同一リクエスト内なら外部 API に行かない
$me = $http->get('https://auth.example.com/me', ['Authorization' => 'Bearer ' . $token])->json();

ヘッダが違えば別キーなので往復します(同じ URL でも認証トークンが違えば別エントリ)。

$http->get($url, ['Authorization' => 'Bearer A']); // 往復
$http->get($url, ['Authorization' => 'Bearer B']); // 往復(別キー)
$http->get($url, ['Authorization' => 'Bearer A']); // ヒット

書き込み系(GET/HEAD 以外)は 送信前にクライアント全体のキャッシュをフラッシュします。「読む → 書く → また読む」で必ず自分の書き込みが見えます。

$user = $http->get('https://api.example.com/users/42')->json();

$http->request('PATCH', 'https://api.example.com/users/42', [], '{"name":"Bob"}');
// ↑ PATCH 送信前にキャッシュを全フラッシュ

// 同じ URL でも再度往復が走り、更新後のレスポンスを返す
$user = $http->get('https://api.example.com/users/42')->json();

フラッシュは クライアント全体(ホスト単位ではない)。api.foo.com を PATCH すると cdn.bar.com の GET キャッシュも消えます。 CachingDatabase と挙動を揃えた単純で安全な選択で、ホスト追跡の機械をこの層に持ち込まない方針です。

サーバ自身がブラウザ/CDN に返すレスポンスのキャッシュ(Cache-Control / ETag)は別物で、HTTP キャッシュと ETag を参照。

タイムアウト#

CurlHttpClient のタイムアウトは環境変数で調整できます(任意・秒。未設定なら cURL 既定)。

変数内容
HTTP_CLIENT_TIMEOUTリクエスト全体のタイムアウト秒
HTTP_CLIENT_CONNECT_TIMEOUT接続確立のタイムアウト秒

環境変数 も参照。

最終更新: 2026-05-21
変更履歴 (4)
  • 本文を更新 (+29 −0)
  • 「リクエストスコープキャッシュ」を追加・「リクエストスコープのメモ化」を削除 (+28 −7)
  • バージョン差分表記(vX.Y.Z 追加/破壊的変更/依存バージョン注記)を削除し現在形に整理
  • 新規作成