====== PSR-7: HTTP message interfaces ======
--- //[[http://www.y2sunlight.com|y2sunlight]] 2020-05-25//
本章は、若干の補足を加筆してはいるものの単に[[https://www.php-fig.org/psr/|PSRのサイト]]を日本語に翻訳したものに過ぎません。英語が堪能な方は原文をご参照下さい。翻訳に当たっては、基本的に機械翻訳を使い、理解できない部分は独断で意訳しております。拙い訳では御座いますが恥を忍んで投稿しておりますので、ご指摘など御座いましたらコメントを頂ければ幸いです。
関連記事
* [[psr:top|PSR - PHP標準勧告]]
* [[psr:psr1|PSR-1: Basic Coding Standard - 基本コーディング規約]]
* [[psr:psr3|PSR-3: Logger Interface - ロガーインターフェイス]]
* [[psr:psr4|PSR-4: Autoloading Standard - オートローディング規約]]
* [[psr:psr5|PSR-5: PHPDoc Standard(Draft) - PHPDoc規約]]
* [[psr:psr6|PSR-6: Caching Interface - キャッシングインターフェイス]]
* PSR-7: HTTP Message Interface - HTTPメッセージインターフェイス
* [[psr:psr11|PSR-11: Container Interface - コンテナインターフェイス]]
* [[psr:psr12|PSR-12: Extended Coding Style - 拡張コーディングスタイル]]
* [[psr:psr13|PSR-13: Link definition interfaces - リンク定義インターフェース]]
* [[psr:psr14|PSR-14: Event Dispatcher - イベントディスパッチャー]]
* [[psr:psr15|PSR-15: HTTP Server Request Handlers - HTTPサーバーリクエストハンドラー]]
* [[psr:psr16|PSR-16: Common Interface for Caching Libraries - キャッシングライブラリのための共通インターフェース]]
* [[psr:psr17|PSR-17: HTTP Factories - HTTPファクトリー]]
* [[psr:psr18|PSR-18: HTTP Client - HTTPクライアント]]
* [[psr:psr19|PSR-19: PHPDoc tags(Draft) - PHPDocタグ]]
-----
====== PSR-7: HTTPメッセージインターフェイス ======
--- // 原文より翻訳 [[https://www.php-fig.org/psr/psr-7/|PSR-7: HTTP message interfaces]] 2020-05-25 現在 //
このドキュメントでは、[[https://tools.ietf.org/html/rfc7230|RFC 7230]] および [[https://tools.ietf.org/html/rfc7231|RFC 7231]] で説明されているHTTPメッセージを表すための一般的なインターフェイスと、[[https://tools.ietf.org/html/rfc3986|RFC 3986]] で説明されているHTTPメッセージで使用するURIについて説明します。
HTTPメッセージはWeb開発の基礎です。WebブラウザーとcURLなどのHTTPクライアントは、HTTPレスポンスメッセージを提供するWebサーバーに送信されるHTTPリクエストメッセージを作成します。サーバー側コードは、HTTPリクエストメッセージを受信し、HTTPレスポンスメッセージを返します。
HTTPメッセージは通常、エンドユーザー(コンシューマー)からは抽象化されていますが、開発者は通常、HTTP APIにリクエストを送信する場合でも、または着信リクエストを処理する場合でも、タスクを実行するために、それらの構造とアクセスまたは操作の方法を知る必要があります。
> 原文では、コンシューマー(consumer)と言う表現が多く見られます。これはエンドユーザーと同じ意味で使用されていると思われます。
すべてのHTTPリクエストメッセージには特定の形式があります:
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
リクエストの最初の行は「リクエストライン」であり、HTTPリクエストメソッド、リクエストターゲット(通常は絶対URIまたはWebサーバー上のパス)、HTTPプロトコルバージョンが順に含まれます。この後には、1行以上のHTTPヘッダー、空行、およびメッセージボディが続きます。
HTTPレスポンスメッセージも同様の構造を持っています:
HTTP/1.1 200 OK
Content-Type: text/plain
This is the response body
最初の行は「ステータスライン」で、HTTPプロトコルのバージョン、HTTPステータスコード、「理由フレーズ」が順に含まれます。「理由フレーズ」とは、ステータスコードの人間が読める説明のことです。 リクエストメッセージと同様に、この後に1行以上のHTTPヘッダー、空行、およびメッセージボディが続きます。
このドキュメントで説明されているインターフェースは、HTTPメッセージとそれらを構成する要素に関する抽象化です。
このドキュメントのキーワード ''MUST'' , ''MUST NOT'' , ''REQUIRED'' , ''SHALL'' , ''SHALL NOT'' , ''SHOULD'' , ''SHOULD NOT'' , ''RECOMMENDED'' , ''MAY'' 及び ''OPTIONAL'' は、 [[https://www.ietf.org/rfc/rfc2119.txt|RFC 2119]]で説明されているように解釈して下さい。
> **RFC 2119の説明**
> ''MUST'', ''REQUIRED'', ''SHALL'' --- 絶対必要
> ''MUST NOT'', ''SHALL NOT'' --- 絶対禁止
> ''SHOULD'', ''RECOMMENDED'' --- 推奨(但し、無視できる特定の正当な理由が存在するかもしれない)
> ''SHOULD NOT'' --- 推奨できない(但し、許可できる特定の正当な理由が存在するかもしれない)
> ''MAY'', ''OPTIONAL'' --- オプション
=== References ===
* [[https://tools.ietf.org/html/rfc2119|RFC 2119]] ・・・ Key words for use in RFCs to Indicate Requirement Levels
* [[https://tools.ietf.org/html/rfc3986|RFC 3986]] ・・・ Uniform Resource Identifier (URI): Generic Syntax
* [[https://tools.ietf.org/html/rfc7230|RFC 7230]] ・・・ Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
* [[https://tools.ietf.org/html/rfc7231|RFC 7231]] ・・・ Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
\\
===== 1. 仕様 ======
==== 1.1 メッセージ ====
HTTPメッセージは、クライアントからサーバーへのリクエスト、またはサーバーからクライアントへのレスポンスです。この仕様は、HTTPメッセージ ''Psr\Http\Message\RequestInterface''''コード'' および ''Psr\Http\Message\ResponseInterface'' に対するインターフェースをそれぞれ定義します。
''Psr\Http\Message\RequestInterface'' と ''Psr\Http\Message\ResponseInterface'' はどちらも ''Psr\Http\Message\MessageInterface'' を拡張(extends)します。''Psr\Http\Message\MessageInterface'' を直接実装(implements)してもかまいませんが( ''MAY'' )、実装者は ''Psr\Http\Message\RequestInterface'' と ''Psr\Http\Message\ResponseInterface'' を実装する必要があります ( ''SHOULD'' )。
これ以降、これらのインターフェースを参照する場合、名前空間 ''Psr\Http\Message'' は省略されます。
\\
==== 1.2 HTTPヘッダー ====
=== Case-insensitiveなヘッダーフィールド名 ===
HTTPメッセージには、Case-insensitiveな(大文字と小文字を区別しない)ヘッダーフィールド名が含まれます。ヘッダーは、Case-insensitiveな方法で ''MessageInterface'' を実装しているクラスから名前で取得されます。たとえば、''foo''ヘッダーを取得する場合、''FoO''''コード''ヘッダーを取得した場合と同じ結果が返されます。 同様に、''Foo''ヘッダーを設定すると、先に設定された''foo''ヘッダー値が上書きされます。
$message = $message->withHeader('foo', 'bar');
echo $message->getHeaderLine('foo');
// Outputs: bar
echo $message->getHeaderLine('FOO');
// Outputs: bar
$message = $message->withHeader('fOO', 'baz');
echo $message->getHeaderLine('foo');
// Outputs: baz
ヘッダーは大文字と小文字を区別せずに取得できますが、特に ''getHeaders()'' を使用して取得する場合は、実装によって元の大文字と小文字を維持する必要があります( ''MUST'' )。
非準拠のHTTPアプリケーションはある特定のケースに依存するかもしれません。従って、ユーザーがリクエストまたはレスポンスを作成する時に、そのケースのHTTPヘッダーを指定できることは有益です。
=== 複数の値を持つヘッダー ===
複数の値を持つヘッダーに対応しつつ、ヘッダーを文字列として操作する便利さを提供するために、ヘッダーは ''MessageInterface'' のインスタンスから配列または文字列として取得できます。''getHeaderLine()'' メソッドを使用して、名前によってCase-insensitiveなヘッダーに対するヘッダー値を文字列として取得して下さい。その文字列には、カンマで連結されたすべてのヘッダー値が含まれています。''getHeader()'' を使用して、名前によって特定のCase-insensitiveなヘッダーに対するすべてのヘッダー値を配列で取得して下さい。
$message = $message
->withHeader('foo', 'bar')
->withAddedHeader('foo', 'baz');
$header = $message->getHeaderLine('foo');
// $header contains: 'bar,baz'
$header = $message->getHeader('foo');
// ['bar', 'baz']
注意:すべてのヘッダー値をカンマを使用して連結できるわけではありません(例えば、''Set-Cookie'')。このようなヘッダーを操作する場合、''MessageInterface'' ベースのクラスのコンシューマーは、そのような複数値のヘッダーを取得するために ''getHeader()'' メソッドに頼るべきです ( ''SHOULD'' )。
=== ホストヘッダー ===
リクエストでは、''Host''ヘッダーは通常、URIのホストコンポーネントと、TCP接続を確立するときに使用されたホストをミラーリングします。 ただし、HTTP仕様では、''Host''ヘッダーが2つのそれぞれのヘッダーと異なることを許可しています。
(リクエストオブジェクトの)構築中、'Host''ヘッダーが提供されていない場合、実装は提供されたURIから'Host''ヘッダーを設定しようと試みる必要があります( ''MUST'' )。
''RequestInterface::withUri()'' は、デフォルトでは、返すべきリクエストの''Host''ヘッダーを、(引数で)渡された ''UriInterface'' のホストコンポーネントと一致するHostヘッダーに置き換えます。
''withUri()'' の2番目の( ''$preserveHost'' )引数に ''true'' を渡すことで、''Host''ヘッダーの元の状態を保持するように選択できます。この引数が ''true'' に設定されている場合、返すべきリクエストは返されたメッセージのホストヘッダーを更新しません(メッセージに''Host''ヘッダーが含まれていない場合を除きます)。
> 上記の原文\\ You can opt-in to preserving the original state of the Host header by passing true for the second ($preserveHost) argument. When this argument is set to true, the returned request will not update the Host header of the returned message – unless the message contains no Host header.
この表は、様々な初期リクエストとURIに対して ''$preserveHost'' 引数を ''true'' に設定して、''withUri()'' によって返されたリクエストに対して ''getHeaderLine('Host')'' が返すものを示しています。
^REQUEST HOST HEADER※1^REQUEST HOST COMPONENT※2^URI HOST COMPONENT※3^RESULT^
|
$request = $request
->withMethod('OPTIONS')
->withRequestTarget('*')
->withUri(new Uri('https://example.org/'));
この例では、最終的に次のようなHTTPリクエストになります:
OPTIONS * HTTP/1.1
しかし、HTTPクライアントは、( ''getUri()'' からの ) 有効なURLを使用して、プロトコル、ホスト名、およびTCPポートを決定することができます。
HTTPクライアントは、''Uri::getPath()'' および ''Uri::getQuery()'' の値を無視し、代わりに ''getRequestTarget()'' によって返される値を使用する必要があります ( ''MUST'' )。''getRequestTarget()'' はデフォルトでは、これら2つの値が連結されます。
4つのリクエストターゲット形式の1つ以上を実装しないことを選択したクライアントは、引き続き ''getRequestTarget()'' を使用する必要があります ( ''MUST'' )。これらのクライアントは、サポートしていないリクエストターゲットを拒否する必要があり( ''MUST'' )、''getUri()'' からの値にフォールバックしてはなりません ( ''MUST NOT'' )。
>''上記の原文'' \\ Clients that choose to not implement 1 or more of the 4 request-target forms, MUST still use getRequestTarget(). These clients MUST reject request-targets they do not support, and MUST NOT fall back on the values from getUri().
''RequestInterface'' は、リクエストターゲットを取得するメソッド、または指定されたリクエストターゲットを使用して新しいインスタンスを作成するメソッドを提供します。デフォルトでは、リクエストターゲットがインスタンス内で具体的に構成されていない場合、''getRequestTarget()'' は、構成されたURIの origin-form(または構成されていない場合は「/」)を返します。
''withRequestTarget($requestTarget)'' は、指定されたリクエストターゲットを使用して新しいインスタンスを作成するので、開発者は他の3つのリクエストターゲット形式 ( absolute-form、authority-form、およびasterisk-form ) を表すリクエストメッセージを作成できます。これが使用された場合、構成されたURIインスタンスは、特にクライアント引き続き使用でき、サーバーへの接続を作成するために使用できます。
\\
==== 1.5 サーバーサイドリクエスト ====
''RequestInterface'' は、HTTPリクエストメッセージの一般的な表現を提供します。ただし、サーバー側の環境の性質上、サーバー側のリクエストには追加の処理が必要です。サーバー側の処理では、Common Gateway Interface(CGI)と、さらに具体的には、PHPのサーバーAPI(SAPI)を介したCGIの抽象化と拡張を考慮する必要があります。PHPは、次のようなスーパーグローバル変数を経由して入力のマーシャリングを簡素化しました。
* **$_COOKIE**: HTTPCookieへのデシリアル化と簡略化されたアクセスを提供します。
* **$_GET**: クエリ文字列引数へのデシリアル化と簡略化されたアクセスを提供します。
* **$_POST**: HTTP POST経由で送信されたURLエンコードされたパラメーターへのデシリアル化と簡略化されたアクセスを提供します。 一般的には、メッセージ本文の解析結果と考えることができます。
* **$_FILES**: ファイルのアップロードに関するシリアル化されたメタデータを提供します。
* **$_SERVER**: CGI / SAPI環境変数へのアクセスを提供します。これには、通常、リクエストメソッド、リクエストスキーム、リクエストURI、およびヘッダーが含まれます。
''ServerRequestInterface'' は ''RequestInterface'' を拡張して、これらのさまざまなスーパーグローバル変数に関する抽象化を提供します。このプラクティスは、コンシューマーによるスーパーグローバル変数への結合を減らすのに役立ち、リクエストコンシューマーをテストする為の能力を奨励し促進します。
サーバーリクエストは、「attributes」という1つの追加プロパティを提供して、コンシューマーがアプリケーション固有のルール(パスマッチング、スキームマッチング、ホストマッチングなど)に対してリクエストを内省(introspect)、分解(decompose)、および照合(match)できるようにします。従って、サーバーリクエストは、複数のリクエストコンシューマー間のメッセージングを提供することもできます。
\\
==== 1.6 アップロードファイル ====
''ServerRequestInterface'' は、正規化された構造でアップロードファイルのツリーを取得するメソッドを規定します。そのツリーの各リーフには ''UploadedFileInterface'' のインスタンスがあります。
''$_FILES'' スーパーグローバル変数には、ファイル入力の配列を処理するときによく知られた問題がいくつかあります。例として、ファイルの配列を送信するフォームがある場合(例えば入力名を「files」とする)、''files[0]'' と ''files[1]'' を送信すると、PHPはこれを次のように表します:
array(
'files' => array(
'name' => array(
0 => 'file0.txt',
1 => 'file1.html',
),
'type' => array(
0 => 'text/plain',
1 => 'text/html',
),
/* etc. */
),
)
期待されるのは次のようです:
array(
'files' => array(
0 => array(
'name' => 'file0.txt',
'type' => 'text/plain',
/* etc. */
),
1 => array(
'name' => 'file1.html',
'type' => 'text/html',
/* etc. */
),
),
)
その結果、コンシューマーはこの言語実装(PHP)の詳細を知って、与えられたアップロードのデータを収集するためのコードを書く必要があります。
さらに、ファイルのアップロードが発生したときに ''$_FILES'' が設定されない状況があります:
* HTTPメソッドが ''POST'' でない場合
* ユニットテストの時
* [[https://reactphp.org/|ReactPHP]]のように非SAPI環境下で操作する場合
そのような場合、データを別の方法でシード(特別な処理)する必要があります。例としては:
* プロセスがメッセージ本文を解析して、ファイルのアップロードを検出する場合があります。 このような場合、実装では、ファイルアップロードをファイルシステムに書き込まず、代わりにストリームにラップして、メモリ、I/O、ストレージのオーバーヘッドを削減します。
* ユニットテストのシナリオでは、さまざまなシナリオを検証および検査するために、開発者はファイルアップロードのメタデータをスタブ化および/またはモックできる必要があります。
''getUploadedFiles()'' は、コンシューマに正規化された構造を提供します。実装では、次のことが期待されます:
* 与えられたファイルアップロードのすべての情報を集約し、それを使用して ''Psr\Http\Message\UploadedFileInterface'' インスタンスに設定します。
* 送信されたツリー構造を再作成します。その時各リーフは、ツリー内の与えられた場所に対して適切な''Psr\Http\Message\UploadedFileInterface'' のインスタンスになります。
参照されるツリー構造は、ファイルが送信されたときのネーミング構造を模倣する必要があります。
最も単純な例では、これは次のように送信された単一の名前付きフォーム要素でしょう:
この場合、''$_FILES'' の構造は次のようになります:
array(
'avatar' => array(
'tmp_name' => 'phpUxcOty',
'name' => 'my-avatar.png',
'size' => 90996,
'type' => 'image/png',
'error' => 0,
),
)
''getUploadedFiles()'' によって返される正規化された形式は次のようになります:
array(
'avatar' => /* UploadedFileInterface instance */
)
名前に配列表記を使用した入力の場合:
$_FILESは次のようになります:
array (
'my-form' => array (
'name' => array (
'details' => array (
'avatar' => 'my-avatar.png',
),
),
'type' => array (
'details' => array (
'avatar' => 'image/png',
),
),
'tmp_name' => array (
'details' => array (
'avatar' => 'phpmFLrzD',
),
),
'error' => array (
'details' => array (
'avatar' => 0,
),
),
'size' => array (
'details' => array (
'avatar' => 90996,
),
),
),
)
そして、''getUploadedFiles()'' によって返される対応するツリーは次のようになります:
array(
'my-form' => array(
'details' => array(
'avatar' => /* UploadedFileInterface instance */
),
),
)
場合によっては、ファイルの配列を指定できます:
Upload an avatar:
Upload an avatar:
(例として、JavaScriptコントロールは、複数のファイルを一度にアップロードできるように、追加のファイルアップロード入力を生成する場合があります。)
このような場合、仕様の実装では、指定されたインデックスにあるファイルに関連するすべての情報を集約する必要があります。その理由は、''$_FILES'' は通常の構造から逸脱しているためです。それは、次のような場合です:
array (
'my-form' => array (
'name' => array (
'details' => array (
'avatar' => array (
0 => 'my-avatar.png',
1 => 'my-avatar2.png',
2 => 'my-avatar3.png',
),
),
),
'type' => array (
'details' => array (
'avatar' => array (
0 => 'image/png',
1 => 'image/png',
2 => 'image/png',
),
),
),
'tmp_name' => array (
'details' => array (
'avatar' => array (
0 => 'phpmFLrzD',
1 => 'phpV2pBil',
2 => 'php8RUG8v',
),
),
),
'error' => array (
'details' => array (
'avatar' => array (
0 => 0,
1 => 0,
2 => 0,
),
),
),
'size' => array (
'details' => array (
'avatar' => array (
0 => 90996,
1 => 90996,
3 => 90996,
),
),
),
),
)
上記の ''$_FILES'' 配列は、''getUploadedFiles()'' によって返される次の構造に対応します:
array(
'my-form' => array(
'details' => array(
'avatars' => array(
0 => /* UploadedFileInterface instance */,
1 => /* UploadedFileInterface instance */,
2 => /* UploadedFileInterface instance */,
),
),
),
)
コンシューマーは、ネストされた配列のインデックス1に次のようにしてアクセスします:
$request->getUploadedFiles()['my-form']['details']['avatars'][1];
アップロードされたファイルデータは派生型(''$_FILES'' またはリクエストボディからの派生)であるため、インターフェイスにはミューテーターメソッド(mutator method) として ''withUploadedFiles()'' も存在し、正規化を別のプロセスに委譲できます。
冒頭の例の場合、使用方法は次のようになります:
$file0 = $request->getUploadedFiles()['files'][0];
$file1 = $request->getUploadedFiles()['files'][1];
printf(
"Received the files %s and %s",
$file0->getClientFilename(),
$file1->getClientFilename()
);
// "Received the files file0.txt and file1.html"
この提案は、実装が非SAPI環境で動作する可能性があることも認識しています。従って、''UploadedFileInterface'' は、環境に関係なく操作が確実に機能するためのメソッドを提供します。 特に:
* ''moveTo($targetPath)'' は、一時的なアップロードファイルで直接 ''move_uploaded_file()'' を呼び出すより安全で推奨される代替手段として提供されています。実装は、環境に基づいて使用する正しい操作を検出します。
* ''getStream()'' は ''StreamInterface'' インスタンスを返します。非SAPIの環境下では、個々のアップロードファイルを解析して直接ファイルにではなく ''php:
// Move a file to an upload directory
// ファイルをアップロードディレクトリに移動する
$filename = sprintf(
'%s.%s',
create_uuid(),
pathinfo($file0->getClientFilename(), PATHINFO_EXTENSION)
);
$file0->moveTo(DATA_DIR . '/' . $filename);
// Stream a file to Amazon S3.
// Assume $s3wrapper is a PHP stream that will write to S3, and that
// Psr7StreamWrapper is a class that will decorate a StreamInterface as a PHP
// StreamWrapper.
//
// ファイルを Amazon S3 にストリーミングします。
// $s3wrapper が S3 に書き込むPHPストリームであり、
// Psr7StreamWrapper が StreamInterface を
// PHP StreamWrapper として装飾するクラスであると仮定します。
$stream = new Psr7StreamWrapper($file1->getStream());
stream_copy_to_stream($stream, $s3wrapper);
\\
===== 2. パッケージ =====
説明されているインターフェースとクラスは、[[https://packagist.org/packages/psr/http-message|psr/http-message]] パッケージの一部として提供されます。
\\
===== 3. インターフェース =====
ここでは、原本に記載されているインターフェースの要約とメソッド一覧を示します。詳細は[[https://www.php-fig.org/psr/psr-7/#3-interfaces|php-fig.org]]を参照してください。
==== 3.1 MessageInterface ====
完全クラス名