RitoLabo

【PHP】PSR-18 HTTP Client(HTTPクライアント)

  • 公開:
  • カテゴリ: PHP PSR
  • タグ: PHP,PSR,PSR-7,PSR-18

PSR-18ドキュメントでは、HTTPリクエストを送信し、HTTPレエスポンスを受信するための一般的なインターフェイスについて説明しています。

アジェンダ
  1. PSR-18の目指すところ
  2. 定義
  3. クライアント
  4. エラーハンドリング
  5. インターフェース
  6. 概要
  7. 理由
  8. 範囲
    1. 範囲とするところ
    2. 範囲としないところ
    3. 非同期HTTPクライアント
  9. アプローチ
    1. デフォルトの動作
    2. 命名の根拠
    3. 例外モデル
    4. 4xxおよび5xxレスポンスの例外スロー
  10. ミドルウェアとクライアントのラッピング
  11. 背景

PSR-18の目指すところ

このPSRの目標は、開発者がHTTPクライアントの実装から切り離されたライブラリを作成できるようにすることです。 これにより依存関係の数が減り、バージョンの競合の可能性が低くなるため、ライブラリがより再利用可能になります。

2つ目の目標は、HTTPクライアントをリスコフの置換原則に従って置き換えることができることです。 これは、リクエストを送信するときにすべてのクライアントが同じように動作する必要があることを意味します。

定義

クライアント
クライアントは、PSR-7互換のHTTPリクエストメッセージを送信し、呼び出し側ライブラリにPSR-7互換のHTTPレスポンスメッセージを返すためにこの仕様を実装するライブラリです。
呼び出しライブラリ
呼び出しライブラリはクライアントを利用するコードです。PSR-18ではインターフェースを実装しませんが、それらを実装するオブジェクト(クライアント)を使用します。

クライアント

クライアントは、ClientInterface を実装するオブジェクトです。

  • 変更されたHTTPリクエストを、提供されたものから送信することを選択します。例えば、発信メッセージbodyを圧縮できます。
  • 呼び出しライブラリに返す前に、受信したHTTPレスポンスを変更することを選択します。例えば、着信メッセージbodyを解凍できます。

クライアントがHTTPリクエストまたはHTTPレスポンスのいずれかを変更することを選択した場合、オブジェクトが内部的に一貫性を保つようにしなければなりません。例えば、クライアントがメッセージ本文の圧縮解除を選択した場合、Content-Encodingヘッダーも削除して、Content-Lengthヘッダーを調整する必要があります。

結果として、PSR-7オブジェクトは不変であるため、呼び出し側ライブラリは、ClientInterface::sendRequest() に渡されるオブジェクトが実際に送信されるPHPオブジェクトであると仮定してはなりません。例えば例外によって返されるRequestオブジェクトは、sendRequest() に渡されたオブジェクトとは異なる可能性があるため、参照による比較(===)は不可能です。

呼び出し元ライブラリに返されるのがステータスコード200以上の有効なHTTPレスポンスになるように、マルチステップ HTTP 1xx レスポンス自体を再構築します。

エラーハンドリング

クライアントは、整形式のHTTPリクエストまたはHTTPレスポンスをエラー状態として扱わないでください。例えば400および500の範囲のレスポンスステータスコードは例外を引き起こしてはならず、通常どおり呼び出しライブラリに返される必要があります。

クライアントはHTTPリクエストをまったく送信できない場合、またはHTTPレスポンスをPSR-7レスポンスオブジェクトで解析できなかった場合にのみ、Psr\Http\Client\ClientExceptionInterface のインスタンスをスローする必要があります。

リクエストメッセージが整形式のHTTPリクエストではないか、重要な情報(ホストやメソッドなど)がないためにリクエストを送信できない場合、クライアントは Psr\Http\Client\RequestExceptionInterface のインスタンスをスローする必要があります。

タイムアウトなど、あらゆる種類のネットワーク障害が原因でリクエストを送信できない場合、クライアントは Psr\Http\Client\NetworkExceptionInterface のインスタンスをスローする必要があります。

クライアントは上記で定義された適切なインターフェイスを実装している場合、ここで定義された例外(たとえば、TimeOutException または HostNotFoundException)よりも具体的な例外をスローできます。

インターフェース

ClientInterface
namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

interface ClientInterface
{
/**
* PSR-7リクエストを送信し、PSR-7レスポンスを返す。
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws \Psr\Http\Client\ClientExceptionInterface リクエストの処理中にエラーが発生した場合
*/
public function sendRequest(RequestInterface $request): ResponseInterface;
}
ClientExceptionInterface
namespace Psr\Http\Client;

/**
* 全てのHTTPクライアント関連の例外は、このインターフェイスを実装する必要があります。
*/
interface ClientExceptionInterface extends \Throwable
{
}
RequestExceptionInterface
namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;

/**
* リクエストが失敗したときの例外
*
* リクエストが無効(例:メソッドが無い)
* ランタイムリクエストエラー(例:ボディストリームがシークできない)
* etc.
*/
interface RequestExceptionInterface extends ClientExceptionInterface
{
/**
* リクエストを返す
*
* リクエストオブジェクトは、ClientInterface::sendRequest()に渡されるオブジェクトとは異なるオブジェクトである場合があります
*
* @return RequestInterface
*/
public function getRequest(): RequestInterface;
}
NetworkExceptionInterface
namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;

/**
* ネットワークの問題のためにリクエストを完了できない場合にスローされる
*
* レスポンスが受信されなかったときにこの例外がスローされるため、レスポンスオブジェクトは無し
* (ターゲットホスト名を解決できないか、接続に失敗した etc.)
*/
interface NetworkExceptionInterface extends ClientExceptionInterface
{
/**
* リクエストを返す
*
* リクエストオブジェクトは、ClientInterface::sendRequest()に渡されるオブジェクトとは異なるオブジェクトである場合がある
*
* @return RequestInterface
*/
public function getRequest(): RequestInterface;
}

概要

HTTPリクエストとレスポンスはWebプログラミングの2つの基本的なオブジェクトです。外部APIと通信するすべてのクライアントは、何らかの形式のHTTPクライアントを使用します。

多くのライブラリは1つの特定のクライアントに結合されているか、クライアントやアダプター層を実装しています。これは、ライブラリの設計不良、バージョンの競合、またはライブラリドメインに関係のないコードにつながります。

理由

PSR-7のおかげで、HTTPリクエストとHTTPレスポンスが理想的にどのように見えるかを知っていますが、リクエストの送信方法とレスポンスの受信方法を定義するものは何もありません。 HTTPクライアントの共通インターフェースにより、ライブラリを特定の実装から切り離すことができます。

範囲

以下、PSR-18が提供するところ、しないところ。

範囲とするところ

PSR-7メッセージを送信し、PSR-7レスポンスを返すための共通インターフェース。

範囲としないところ

  • 非同期HTTPリクエストのサポートは、別の将来のPSRに残されています。
  • このPSRではHTTPクライアントの構成方法を定義しません。デフォルトの動作のみを指定します。
  • このPSRはミドルウェアの使用に関して中立です。

非同期HTTPクライアント

非同期リクエストがこのPSRでカバーされない理由は、Promiseの共通標準の欠如です。Promiseは独自の仕様に値するほど十分に複雑であり、この仕様にラップされるべきではありません。

Promise PSRが受け入れられると、非同期リクエストの個別のインターフェイスを個別のPSRで定義できます。非同期呼び出しの戻り値の型はPromiseになるため、非同期リクエストのメソッドシグネチャは同期リクエストのメソッドシグネチャと異なる必要があります。したがってこのPSRは上位互換性があり、クライアントは公開したい機能に基づいて1つまたは両方のインターフェイスを実装できます。

アプローチ

以下、策定に関するアプローチについて

デフォルトの動作

このPSRの意図は、明確に定義された動作を持つHTTPクライアントをライブラリ開発者に提供することです。ライブラリはクライアント実装の詳細を処理するための特別なコードなしで準拠クライアントを使用できる必要があります(リスコフの置換原則)。

PSRは、HTTPクライアントの構成方法を制限または定義しようとしません。

別のアプローチは、構成をクライアントに渡すことです。このアプローチにはいくつかの欠点があります。

  • 構成はPSRで定義する必要があります。
  • すべてのクライアントは、定義された構成をサポートする必要があります。
  • クライアントに構成が渡されない場合、動作は予測できません。

命名の根拠

メインインターフェイスの動作は、sendRequest(RequestInterface $request):ResponseInterfaceメソッドによって定義されます。

より短いメソッド名 send() が提案されていますが、これはGuzzleなどの既存の非常に一般的なHTTPクライアントですでに使用されています。

そのためこの標準を採用する場合、仕様を実装するために後方互換性を破る必要があります。 代わりに sendRequest() を定義することにより、BCが即座に中断することなく採用できるようにします。

例外モデル

ドメイン例外 NetworkExceptionInterface および RequestExceptionInterface は、互いに非常に類似したコントラクトを定義します。

選択されたアプローチは、継承がドメインモデルで意味をなさないため互いに拡張しないようにすることです。RequestExceptionInterface は、単に NetworkExceptionInterface ではありません。

RequestAwareException および(または) ResponseAwareException インターフェースを拡張するための例外の許可については説明しましたが、これは取るべきではない便利なショートカットです。むしろ特定の例外をキャッチして、それに応じて処理する必要があります。

例外を定義するときはより細かくすることができます。 例えば TimeOutException および HostNotFoundExceptionNetworkExceptionInterface のサブタイプである可能性があります。選択されたアプローチはそのようなサブタイプを定義することではありません。消費ライブラリでの例外処理はほとんどの場合、それらの例外間で変わらないからです。

4xxおよび5xxレスポンスの例外スロー

最初のアイデアは、HTTPステータス4xxおよび5xxのレスポンスに対して例外をスローするようにクライアントを構成できるようにすることでした。ライブラリを使用すると、4xxおよび5xxのレスポンスを2回チェックする必要があるため、このアプローチは望ましくありません。

仕様をより予測可能にするために、HTTPクライアントは4xxおよび5xxレスポンスに対して例外をスローしないことが決定されました。

ミドルウェアとクライアントのラッピング

仕様では、HTTPクライアントをラップ/装飾するミドルウェアまたはクラスに制限を設けていません。装飾クラスも ClientInterface を実装する場合は、仕様にも従う必要があります。

HTTPクライアントに設定を許可したりミドルウェアを追加したりして、リダイレクトを追跡したり例外をスローしたりすることができます。それがアプリケーション開発者の決定である場合、仕様を破りたいと明確に述べています。これは、アプリケーション開発者が処理すべき問題(または機能)です。

サードパーティのライブラリは、HTTPクライアントが仕様に違反していると想定してはなりません。

背景

HTTPクライアントPSRは、php-httpチームによってインスピレーションを得て作成されました。 2015年に、彼らはHTTPlugをHTTPクライアントの共通インターフェースとして作成しました。

彼らは、特定のHTTPクライアント実装に依存しないように、サードパーティのライブラリが使用できる抽象化を望んでいました。 2016年1月に安定バージョンがタグ付けされ、それ以来プロジェクトは広く採用されています。

最初の安定バージョンから2年間で300万回以上のダウンロードがあったため、この「事実上の」標準を正式な仕様とすることとしました。