RitoLabo

【PHP】PSR-17 HTTP Factories(HTTPファクトリ)

  • 公開:
  • 更新:
  • カテゴリ: PHP PSR
  • タグ: PHP,PSR,PSR-7,PSR-17

アジェンダ
  1. 仕様
  2. インターフェース
    1. RequestFactoryInterface
    2. ResponseFactoryInterface
    3. ServerRequestFactoryInterface
    4. StreamFactoryInterface
    5. UploadedFileFactoryInterface
    6. UriFactoryInterface
  3. 概要
  4. 理由
  5. 範囲
    1. 範囲とするところ
    2. 範囲としないところ
  6. アプローチ
    1. 選択されたアプローチ
    2. 既存の実装
      1. Diactoros
      2. Guzzle
      3. Slim
    3. 潜在的な問題
  7. 設計上の決定
    1. PHP 7を選ぶ理由
    2. 複数のインターフェースが必要な理由
    3. ResponseFactoryInterfaceへの$reasonPhrase引数が存在するのはなぜか?
    4. ServerRequestFactoryInterfaceへの$serverParams引数が存在するのはなぜか?
    5. スーパーグローバルからServerRequestInterfaceを作成するファクトリーがないのはなぜか?
    6. RequestFactoryInterface::createRequestが文字列URIを許可するのはなぜか?

仕様

HTTP factoryは、PSR-7で定義されている新しいHTTPオブジェクトを作成するものです。

HTTP factoryは、パッケージによって提供される各オブジェクトタイプに対してこれらのインターフェイスを実装する必要があります。

インターフェース

次のインターフェイスは、単一のクラス内または個別のクラス内で一緒に実装できます。

RequestFactoryInterface

クライアントリクエストを作成する機能を持ちます。

namespace Psr\Http\Message;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

interface RequestFactoryInterface
{
/**
* 新しいリクエストを作成する
*
* @param string $method リクエストに関連付けられたHTTPメソッド
* @param UriInterface|string $uri リクエストに関連付けられたURI
*/
public function createRequest(string $method, $uri): RequestInterface;
}

ResponseFactoryInterface

レスポンスを作成する機能を持ちます。

namespace Psr\Http\Message;

use Psr\Http\Message\ResponseInterface;

interface ResponseFactoryInterface
{
/**
* 新しいレスポンスを作成する
*
* @param int $code HTTPステータスコード デフォルト=200
* @param string $reasonPhrase
* 生成された応答のステータスコードに関連付ける理由フレーズ
* 何も提供されない場合、実装はHTTP仕様で提案されているデフォルトを使用できる
*/
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
}

ServerRequestFactoryInterface

サーバーリクエストを作成する機能を持ちます。

<?php
namespace Psr\Http\Message;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;

interface ServerRequestFactoryInterface
{
/**
* サーバーリクエストを作成する
*
* サーバーパラメーターは指定されたとおりに正確に取得されることに注意してください。
* 指定された値の解析/処理は実行されません。 特に、HTTPメソッドまたはURIを決定する試みは行われません。
* これらは明示的に提供する必要があります。
*
* @param string $method リクエストに関連付けられたHTTPメソッド
* @param UriInterface|string $uri リクエストに関連付けられたURI
* @param array $serverParams サーバーAPI(SAPI)パラメーターの配列
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
}

StreamFactoryInterface

リクエストとレスポンスのストリームを作成する機能を持ちます。

<?php
namespace Psr\Http\Message;

use Psr\Http\Message\StreamInterface;

interface StreamFactoryInterface
{
/**
* 文字列から新しいストリームを作成する
*
* ストリームは一時リソースで作成する必要があります(SHOULD)
*
* @param string $content ストリームに入力する文字列コンテンツ
*/
public function createStream(string $content = ''): StreamInterface;

/**
* 既存のファイルからストリームを作成する
*
* ファイルは指定されたモードを使用して開く必要があります(MUST)
* これはfopen関数でサポートされている任意のモードです
*
* $filenameはfopen()でサポートされている任意の文字列(MAY)
*
* @param string $filename ストリームのベースとして使用するファイル名またはストリームURI
* @param string $mode 基になるファイル名/ストリームを開くモード
*
* @throws \RuntimeException ファイルを開けない場合
* @throws \InvalidArgumentException モードが無効な場合
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;

/**
* 既存のリソースから新しいストリームを作成する
*
* ストリームは読み取り可能でなければならない。書き込み可能の場合もある。
*
* @param resource $resource ストリームのベースとして使用するPHPリソース
*/
public function createStreamFromResource($resource): StreamInterface;
}

このインターフェイスの実装では、文字列からリソースを作成するときに一時的なストリームを使用する必要があります。推奨される方法は次のとおりです。

$resource = fopen('php://temp', 'r+');

UploadedFileFactoryInterface

アップロードされたファイルのストリームを作成する機能を持ちます。

<?php
namespace Psr\Http\Message;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;

interface UploadedFileFactoryInterface
{
/**
* 新しいアップロードファイルを作成する
*
* サイズが指定されていない場合はストリームのサイズを確認して決定される
*
* @link http://php.net/manual/features.file-upload.post-method.php
* @link http://php.net/manual/features.file-upload.errors.php
*
* @param StreamInterface $stream アップロードされたファイルコンテンツを表す基になるストリーム
*
* @param int $size ファイルサイズ(bytes)
* @param int $error PHP アップロードエラーコード
* @param string $clientFilename クライアントが提供するファイル名etc
* @param string $clientMediaType クライアントが提供するメディアタイプetc
*
* @throws \InvalidArgumentException ファイルリソースが読めない場合
*/
public function createUploadedFile(
StreamInterface $stream,
int $size = null,
int $error = \UPLOAD_ERR_OK,
string $clientFilename = null,
string $clientMediaType = null
): UploadedFileInterface;
}

UriFactoryInterface

クライアントおよびサーバーリクエストのURIを作成する機能を持ちます。

namespace Psr\Http\Message;

use Psr\Http\Message\UriInterface;

interface UriFactoryInterface
{
/**
* 新しいURIを作成する
*
* @param string $uri パースするURI
*
* @throws \InvalidArgumentException 指定されたURIをパースできない場合
*/
public function createUri(string $uri = '') : UriInterface;
}

概要

このPSRの目的は、PSR-7オブジェクトを作成するメソッドを定義するFactoryインターフェースを提供することです。

理由

PSR-7の現在の仕様では、不変のコピーを作成することにより、ほとんどのオブジェクトを変更できます。ただし、2つの注目すべき例外があります。

  • StreamInterfaceは、リソースが書き込み可能な場合にのみリソースへの書き込みを許可する、リソースに基づく可変オブジェクトである。
  • UploadedFileInterfaceは、変更機能を提供しないリソースに基づく読み取り専用オブジェクトである。

前者は、レスポンスを不完全な状態のままにする可能性があるため、PSR-7ミドルウェアの重大な問題点です。レスポンス本文に添付されたストリームがシーク可能でも書き込み可能でもない場合、本文が既に書き込まれているエラー状態から回復する方法はありません。

このシナリオは、新しいストリームを作成するFactoryを提供することで回避できます。HTTP Object Factoryの正式な標準がないため、開発者はこれらのオブジェクトを作成するために特定のベンダーの実装に依存する必要があります。

別の問題点は、再利用可能なミドルウェアまたはリクエストハンドラを作成するときです。そのような場合、パッケージの作成者はレスポンスを作成して返す必要があります。

ただし、個別のインスタンスを作成すると、パッケージが特定のPSR-7実装に結び付けられます。これらのパッケージが代わりにリクエストファクトリインターフェイスに依存している場合、PSR-7実装にとらわれないままにすることができます。

Factoryの正式な標準を作成すると、開発者は特定の実装への依存を回避しながら、必要に応じて新しいオブジェクトを作成できます。

範囲

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

範囲とするところ

PSR-7互換オブジェクトを作成するメソッドを定義するインターフェースのセットを提供します。

範囲としないところ

PSR-7ファクトリーの特定の実装を提供すること。

アプローチ

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

選択されたアプローチ

ファクトリメソッド定義は、インスタンス化後にオブジェクトを変更できるかどうかに基づいて選択されています。変更できないインターフェイスの場合、インスタンス化時にすべてのオブジェクトプロパティを定義する必要があります。

UriInterfaceの場合、便宜上完全なURIを渡すことができます。

使用されるメソッド名は競合しません。これにより、適切な場合に単一のクラスで複数のインターフェイスを実装できます。

既存の実装

PSR-7の現在の実装はすべて、独自の要件を定義しています。ほとんどの場合、必要なパラメーターは、提案されているファクトリーメソッドと同じかそれよりも厳密ではありません。

Diactoros

Diactorosは、サーバー使用のための最初のHTTPメッセージ実装の1つであり、PSR-7仕様と並行して開発されました。

Request
必須パラメーター、メソッド、およびURIのデフォルトはヌルではありません。
Response
必須パラメーターはありません。ステータスコードのデフォルトは200です。
ServerRequest
必須パラメーターはありません。グローバルからリクエストを作成するための個別のServerRequestFactoryが含まれています。
Stream
bodyには string|resource $stream が必要です。
UploadedFile
string|resource $streamOrFile・int $size・int $errorStatus が必要です。エラーステータスは、PHPアップロード定数でなければなりません。
Uri
必須パラメーターはありません。文字列$uriはデフォルトでは空です。

全体的にこのアプローチは提案されたFactoryに非常に似ています。場合によっては、有効なオブジェクトには不要なオプションがDiactorosによって提供されます。

PSR-17アップロードファイルファクトリでは、サイズとエラーステータスをオプションにすることができます。

Guzzle

Guzzleは、クライアントの使用に焦点を当てたHTTPメッセージ実装です。

Request
文字列 $method と string|UriInterface $uri の両方が必要です。
Response
必須パラメーターはありません。ステータスコードのデフォルトは200です。
Stream
bodyにリソース $stream が必要です。
Uri
必須パラメーターはありません。文字列 $uri はデフォルトでは空です。

Guzzleには、クライアントの使用を対象とするため、ServerRequestまたはUploadedFileの実装は含まれていません。

全体的にこのアプローチは提案された(PSR-17)Factoryと非常に似ています。注目すべき違いの1つは、Guzzleではリソースでストリームを構築する必要があり、文字列を許可しないことです。

ただし、コンテンツの文字列からストリームを作成するヘルパー関数stream_forと、ファイルパスからリソースを作成するtry_fopen関数が含まれています。

Slim

Slimは、バージョン3.0以降のHTTPメッセージを利用するPHPマイクロフレームワークです。

Request
文字列 $method / UriInterface $uri / HeadersInterface $headers / 配列 $cookies / 配列 $serverParams / StreamInterface $body が必要です。
ファクトリメソッド createFromEnvironment(Environment $environment)が含まれます。このメソッドはフレームワーク固有ですが、提案されている createServerRequestFromArray に類似しています。
Response
必須パラメーターはありません。ステータスコードのデフォルトは200です。
Stream
bodyにリソース $streamが必要です。
UploadedFile
ソースファイルに文字列 $file が必要です。
$_FILESまたは同様の形式からUploadedFileインスタンスの配列を作成するためのファクトリメソッド parseUploadedFiles(array $uploadedFiles)が含まれています。
フレームワーク固有で、parseUploadedFilesを使用するファクトリメソッド createFromEnvironment(Environment $env)が含まれています。
Uri
文字列 $scheme および、文字列 $hostが必要です。
文字列からUriを作成するために使用できるファクトリメソッド createFromString($uri)が含まれています。

サーバーの使用のみを対象としているため、SlimにはRequestの実装は含まれていません。上記の実装は、ServerRequestの実装です。

比較されたアプローチの中で、Slimは提案されたFactoryと最も異なります。最も注目すべきは、リクエストの実装にはHTTPメッセージ仕様で定義されていないフレームワーク固有の要件が含まれていることです。

潜在的な問題

この標準を確立する上で最も難しいタスクは、インターフェイスのメソッドシグネチャを定義することです。PSR-7には明示的に必要な値について明確な宣言がないため、読み取り専用のプロパティはインターフェイスにオブジェクトをコピーおよび変更するメソッドがあるかどうかに基づいて推測する必要があります。

設計上の決定

以下、設計上の決定について

PHP 7を選ぶ理由

PSR-7はPHP 7を対象としていませんが、この仕様の著者は執筆時点(2018年4月)でPHP 5.6が15か月前にバグ修正の受信を停止し、8か月後にセキュリティパッチを受信しなくなることに注意しています。

PHP 7.0自体は、2018年12月3にセキュリティサポートが停止(現在のサポートの詳細については、PHPのサポートされているバージョンのドキュメントを参照してください)。

補足
PHP 7.0は2018年12月3にセキュリティサポートが停止しました。

仕様は長期的なものであることを意図しているため著者は、仕様は予見可能な将来にサポートされるバージョンを対象とすべきだと感じています。

PHP 5はサポートしません。そのためセキュリティの観点からPHP 7の下で何かをターゲットにすることは、サポートされていないPHPバージョンの使用を暗黙のうちに承認するため、ユーザーにとっては害になります。

さらに、同様に重要なこととして、PHP 7では、定義するインターフェイスに戻り値のタイプのヒントを提供できます。これにより、エンドユーザーは実装によって返される値がまさに期待どおりになると想定できるため、強力で予測可能な契約が保証されます。

複数のインターフェースが必要な理由

提案された各インターフェイスは、(主に)1つのPSR-7タイプの生成を担当します。これにより、消費者は必要なものを正確に入力できます。レスポンスが必要な場合は、ResponseFactoryInterfaceで入力します。

URIが必要な場合は、UriFactoryInterfaceでタイプヒントします。このようにして、ユーザーは必要なものについてきめ細かく設定できます。

これによりアプリケーション開発者は、使用しているPSR-7実装に基づいて匿名の実装を提供し、特定のコンテキストに必要なインスタンスのみを生成できます。結果、定型文が削減されます。開発者は、未使用のメソッドのスタブを作成する必要はありません。

ResponseFactoryInterfaceへの$reasonPhrase引数が存在するのはなぜか?

ResponseFactoryInterface::createResponse() には、オプションの文字列引数 $reasonPhrase が含まれています。

PSR-7仕様では、ステータスコードを提供すると同時に理由フレーズを提供できます。これは、2つが関連するデータであるためです。

この仕様の作成者は、PSR-7 ResponseInterface::withStatus()署名を模倣して、作成された応答に両方のデータセットが存在することを保証することを選択しました。

ServerRequestFactoryInterfaceへの$serverParams引数が存在するのはなぜか?

ServerRequestFactoryInterface::createServerRequest() には、オプションの $serverParams 配列引数が含まれます。

これが提供される理由は、サーバーのパラメーターが設定された状態でインスタンスを作成できるようにするためです。

ServerRequestInterfaceを介してアクセス可能なデータのうち、ミューテーターメソッドを持たないデータは、サーバーのパラメーターに対応するデータのみです。

そのためこのデータは最初の作成時に提供する必要があり、ファクトリメソッドへの引数として存在します。

スーパーグローバルからServerRequestInterfaceを作成するファクトリーがないのはなぜか?

ServerRequestFactoryInterface の主な使用例は、既知のデータから新しい ServerRequestInterface インスタンスを作成することです。

スーパーグローバルからのデータのマーシャリングに関するソリューションは、次のことを前提としています。

  • スーパーグローバルが存在する
  • スーパーグローバルは特定の構造に従う

これらの2つの仮定は常に当てはまるわけではありません。Swoole、ReactPHPなどの非同期システムを使用する場合

  • $_GET/$_POST/$_COOKIE、および$_FILESなどの標準的なスーパーグローバルを設定しません
  • $_SERVERに標準のSAPI(mod_php、mod-cgi、mod-fpmなど)と同じ要素を追加しません

さらに、さまざまな標準SAPIがさまざまな情報を$_SERVERに提供しリクエストヘッダーにアクセスするため、リクエストの初期設定にはさまざまなアプローチが必要です。

そのため、スーパーグローバルからインスタンスを作成するためのインターフェイスの設計はこの仕様の範囲外であり、主に実装固有のものである必要があります。

RequestFactoryInterface::createRequestが文字列URIを許可するのはなぜか?

RequestFactoryInterface の主な使用例は、リクエストを作成することです。リクエストに必要な値は、リクエストメソッドとURIのみです。

RequestFactoryInterface::createRequest()UriInterfaceインスタンスを受け入れることができますが、文字列も許可します。

理論的根拠は2つあります。まず、大部分のユースケースはリクエストインスタンスを作成することです。

URIインスタンスの作成は二次的です。UriInterface を必要とするということは、ユーザーが UriFactoryInterface にアクセスする必要があるか、 RequestFactoryInterfaceがUriFactoryInterface に厳しい要件を持つことを意味します。

1つ目はFactoryの消費者の使用を複雑にし、2つ目はFactoryの開発者またはFactoryインスタンスを作成する開発者の使用を複雑にします。

第二に、UriFactoryInterfaceUriInterfaceインスタンスを作成する1つの方法を提供します。これは文字列URIからのものです。

URIの作成が文字列に基づいている場合、RequestFactoryInterface が同じセマンティクスを許可しない理由はありません。さらに、この提案が開発された時点で調査されたすべてのPSR-7実装では、RequestInterfaceインスタンスを作成するときに、提供されたUriInterface実装に値が渡されるため、文字列URIが許可されました。そのため、文字列を受け入れることは適切であり、既存のセマンティクスに従います。