ドキュメント本文へスキップ
PHP

HTTP クライアント

既定では cURL、独自のものを使いたいときは任意の PSR-18 クライアント、そしてテスト用のフェイク。

既定のクライアント

httpClient: を渡さない場合、リクエストは OpenEmail\Http\CurlHttpClient を通ります。これに必要なのは curl 拡張だけです。クライアントごとに 1 つの cURL ハンドルを保持するため、2 回目のリクエストは 1 回目が開いた接続を再利用します。プロキシ、認証局、追加の cURL 設定のオプションは「設定」のページに一覧があります。

バイト列を何が運ぶにせよ、それ以外はすべてクライアント自身が行います。資格情報、冪等性キー、リトライとそのバックオフ、試行ごとのタイムアウト、失敗ごとの例外です。HTTP クライアントは 1 つのリクエストを送ってレスポンスを返すだけです。

任意の PSR-18 クライアント

OpenEmail\Http\Psr18HttpClient は、Guzzle や Symfony HttpClient など PSR-18 を実装する任意のクライアントをラップします。そのためリクエストは、アプリケーションですでに設定済みのクライアントを、そのミドルウェア、ログ、プロキシ設定とともに通ります。

guzzle.php
use GuzzleHttp\Client;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client())); $client->me->ping();
symfony_http_client.php
use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail;use Symfony\Component\HttpClient\HttpClient;use Symfony\Component\HttpClient\Psr18Client; $http = new Psr18Client(HttpClient::create(['timeout' => 30, 'max_redirects' => 0])); $client = new OpenEmail(httpClient: new Psr18HttpClient($http)); $client->me->ping();

PSR-18 は PSR-17 ファクトリーでリクエストを組み立てます。アダプターは、Symfony の Psr18Client のようにクライアント自身がファクトリーでもあればそれを使い、次にインストールされていれば php-http/discovery を、その次に nyholm/psr7 か Guzzle 自身のファクトリーを使います。どれも見つからなければ、何をインストールすべきかを示して InvalidArgumentException をスローします。自分で選ぶには requestFactory: と streamFactory: を渡してください。Symfony の Psr18Client には PSR-18 のインターフェースと PSR-17 の実装も必要なため、composer require symfony/http-client psr/http-client nyholm/psr7 でインストールしてください。

factories.php
use GuzzleHttp\Client;use Nyholm\Psr7\Factory\Psr17Factory;use OpenEmail\Http\Psr18HttpClient;use OpenEmail\OpenEmail; $factory = new Psr17Factory(); $client = new OpenEmail(httpClient: new Psr18HttpClient(new Client(), requestFactory: $factory, streamFactory: $factory)); $client->me->ping();
項目起きること
タイムアウトGuzzle はすべてのリクエストでクライアントの timeout: を受け取ります。PSR-18 にはそれ以外のクライアントにタイムアウトを渡す方法がないため、上の HttpClient::create(['timeout' => 30, 'max_redirects' => 0]) のように、ラップするクライアント側で設定してください。そうしないと、応答のないリクエストがいつまでも待ち続けることがあります。
エラーステータスPSR-18 クライアントは 4xx と 5xx を例外にせずレスポンスとして返し、アダプターは Guzzle にも同じようにさせます。そのため、どの拒否も正しい ApiException になります。
応答なしラップしたクライアントからの ClientExceptionInterface は、そのメッセージを保った NetworkException になりますが、例外そのものは保持しません。その例外がリクエストとその Authorization ヘッダーを持っているからです。メッセージがリクエストのタイムアウトを示しているときは isTimeout() が true になり、Guzzle 以外のクライアントでは、それがラップしたクライアント自身のタイムアウトだったことがメッセージに示されます。
リダイレクトGuzzle にはリダイレクトに従わないよう指示し、Symfony の例では max_redirects を 0 に設定しています。そのため、既定の cURL クライアントと同じく、リダイレクトは ApiException になります。

独自のクライアント

httpClient: は、OpenEmail\Http\HttpClient を実装するものなら何でも受け取ります。これはメソッドが send(HttpRequest $request): HttpResponse の 1 つだけのインターフェースです。4xx と 5xx を含むすべてのステータスでレスポンスを返し、レスポンスが届かなかったときだけ例外をスローしてください。

クラス持っているもの
HttpRequestmethod、url、名前と値の配列である headers、文字列または null の body、秒単位の timeout(なしの場合は null)。header($name) は大文字小文字を問わずヘッダーを 1 つ読みます。var_dump()、print_r()、json_encode() では Authorization ヘッダーが [redacted] と表示されますが、var_export() と Symfony の dump() は headers をそのまま出力するため、この 2 つでリクエストをログに残さないでください。
HttpResponsenew HttpResponse($status, $headers, $body) で作ります。ヘッダー名は小文字に変換され、header($name) は大文字小文字を問わずヘッダーを 1 つ読みます。
HttpClientExceptionレスポンスが届かなかったときに send からスローします。new HttpClientException($message, timeout: true) はタイムアウトであることを示します。

ネットワークなしでのテスト

フェイクのクライアントは、送信されるはずだった内容を記録し、テストに必要な応答を返します。そのため何もマシンの外に出ず、テストでは送信された内容を正確に確認できます。

fake_http_client.php
use OpenEmail\Http\HttpClient;use OpenEmail\Http\HttpRequest;use OpenEmail\Http\HttpResponse;use OpenEmail\OpenEmail; $fake = new class implements HttpClient {    public array $requests = [];     public function send(HttpRequest $request): HttpResponse    {        $this->requests[] = $request;         return new HttpResponse(            200,            ['content-type' => 'application/json'],            json_encode(['id' => 'msg_test', 'status' => 'sent', 'replayed' => false], JSON_THROW_ON_ERROR),        );    }}; $testClient = new OpenEmail(apiKey: 'oe_test_fake', httpClient: $fake, maxRetries: 0, disableUpdateNotice: true); $sent = $testClient->emails->send([    'from' => '[email protected]',    'to' => '[email protected]',    'subject' => 'Hi',    'text' => 'Hello',]); $request = $fake->requests[0]; echo $sent['status'], ' ', $request->method, ' ', $request->url, ' ', $request->header('Idempotency-Key'), PHP_EOL;var_dump($request);
  • 2xx 以外のステータスを、{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}} のような API のエラーエンベロープをボディにして返すと、対応する ApiException のサブクラスが得られます。
  • send から new HttpClientException('timed out', timeout: true) をスローすると、isTimeout() が true の NetworkException が得られます。RuntimeException のようなそれ以外の Exception は、isTimeout() が false の NetworkException になります。
  • LogicException と、TypeError のようなあらゆる Error は、フェイクのバグとみなされます。そのままスローされ、リトライされることはありません。

失敗をスクリプトで再現するときは、テスト用クライアントを maxRetries: 0 で作ってください。そうしないと、繰り返しても安全な呼び出しでのリトライ可能なステータスやネットワーク障害が、実際の待機を挟んで 3 回試行されます。