## PHP 拡張モジュールの作者用のストリーム API

> 注意:

> この章で説明する関数は PHP のソースコード内で用いられるものであり、 PHP の関数ではありません。PHP のユーザー用のストリーム関数については ストリーム関数のリファレンス をご覧ください。

### 概要

PHPストリームAPIは、PHP拡張モジュールにファイルおよびソケット処理 用の統一化された手段を導入するものです。 共通の操作を行なうための標準関数を有する単一のAPIを使用することに より、ストリームAPIは拡張モジュールがファイル、ソケット、URL、メモリ スクリプトが定義したオブジェクトにアクセスすることを可能にします。 ストリームは、新規ストリームを登録するために動的にロードされる モジュール(およびスクリプト!)とすることができる実行時に拡張可能な APIです。

ストリームAPIの目的は、ファイル、URL、その他のストリームにできるデータ ソースを平易な統一されたAPIにより、開発者が容易にオープンできるように することです。APIは、ほぼANSI C stdio関数と(多くの主な関数について同等 の意味を有しており、)類似しています。このため、Cプログラマは、 ストリームに慣れている印象を受けるはずです。

ストリームAPIは、いくつかの異なるレベルを処理します。 基本レベルでは、APIはストリーマブルなデータソースを表す php_streamオブジェクトを定義します。 やや高いレベルでは、APIは、URLからのデータおよびメタデータの取得を サポートするために低レベルAPIをラップしたphp_stream_wrapper オブジェクトを定義します。 追加のパラメータ context は、 ほとんどのストリーム作成関数で使用できます。 これはラッパーの stream_opener メソッドに渡され、ラッパーの挙動を微調整します。

あらゆるストリームは、一度オープンされると任意の数の filters を適用することができます。 これは、ストリームがデータを読み書きする際にそのデータを加工します。

ストリームは、ファイル処理の他の形式にキャスト(変換)でき、 大きな問題もなくサードパーティ製のライブラリと組み合わせて使用する ことができます。これにより、これらのライブラリがURLソースからデータに 直接アクセスできるようになります。 使用するシステムにfopencookie()または funopen()関数がある場合、 任意のPHPストリームをANSI stdioを使う任意のライブラリに渡すことさえ できます!

### ストリームの基本

ストリームの使用方法は、ANSI stdio関数の使用と非常に似ています。 主な違いは、使用を開始するストリームを得る方法です。 多くの場合、ストリームのハンドルを得るために php_stream_open_wrapper()を使用します。 この関数の動作は、以下の例で示すようにfopenと非常によくにています。

例1 PHPホームページを表示するための簡単なストリームの例

    php_stream * stream = php_stream_open_wrapper("http://www.php.net", "rb", REPORT_ERRORS, NULL);
    if (stream) {
        while(!php_stream_eof(stream)) {
            char buf[1024];

            if (php_stream_gets(stream, buf, sizeof(buf))) {
                printf(buf);
            } else {
                break;
            }
        }
        php_stream_close(stream);
    }

以下の表にその他の一般的なANSI stdio関数と等価なストリーム関数を 示します。注記で除外されていない限り、関数の意味は同じです。

ANSI stdioと等価なストリームAPI関数
ANSI stdio関数 | PHP ストリーム関数 | 注意
--- | --- | ---
fopen | php_stream_open_wrapper | ストリームではパラメータが増えています
fclose | php_stream_close |
fgets | php_stream_gets |
fread | php_stream_read | パラメータnmembの値を1と仮定すると、プロトタイプはread(2)により似ることになります
fwrite | php_stream_write | パラメータnmembの値を1と仮定すると、プロトタイプはwrite(2)により似ることになります
fseek | php_stream_seek |
ftell | php_stream_tell |
rewind | php_stream_rewind |
feof | php_stream_eof |
fgetc | php_stream_getc |
fputc | php_stream_putc |
fflush | php_stream_flush |
puts | php_stream_puts | fputsではなく、putsと同じ意味
fstat | php_stream_stat | ストリームはより情報の多いstat構造体を有しています

### リソースとしてのストリーム

すべてのストリームは、作成されるとリソースとして登録されます。これにより、 たとえ致命的なエラーが発生したとしても適切な後処理が行われることが保障されます。 PHP のすべてのファイルシステム関数は、ストリームリソースに対して操作することができます。 つまり、あなたの作成した拡張モジュールは、 通常の PHP ファイルポインタをパラメータとして受け取って 結果をストリームで返すことができるということです。 ストリーム API により、この処理が楽にできるようになっています。

例2 ストリームをパラメータとして受け取る方法

    PHP_FUNCTION(example_write_hello)
    {
        zval *zstream;
        php_stream *stream;

        if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &zstream))
            return;

        php_stream_from_zval(stream, &zstream);

        /* これでストリームを使うことができます。しかし、ストリームの
           "所有者" はこの関数ではなく、呼び出し元のスクリプトです。
           つまり、この関数内でストリームを閉じてはいけないということです。
           そんなことをすると PHP がクラッシュしてしまいます! */

        php_stream_write(stream, "hello\n");

        RETURN_TRUE();
    }

例3 関数からストリームを返す方法

    PHP_FUNCTION(example_open_php_home_page)
    {
        php_stream *stream;

        stream = php_stream_open_wrapper("http://www.php.net", "rb", REPORT_ERRORS, NULL);

        php_stream_to_zval(stream, return_value);

        /* これ以降、ストリームの "所有者" は呼び出し元スクリプトとなります。
           もしここでストリームを閉じると、PHP がクラッシュしてしまいます! */
    }

ストリームの後始末が自動的に行われることから、 わざわざ後始末を気にしたりしないずさんなプログラマでいても大丈夫と思われるかもしれません。 確かにそれでもうまくいくでしょうが、いくつかの理由からこれはお勧めできません。 ストリームのオープン中はシステムリソースがロックされるので、 使用済みのファイルをオープンしたままにしておくと、 他のプロセスがファイルにアクセスできなくなります。 大量のファイルを扱うスクリプトでは、 使用済みのリソースを溜め込み続けるとメモリやファイル記述子の番号がいっぱいになってしまいます。 その結果ウェブサーバーがリクエストを受け付けられないようになります。 どうです? あまりいい話ではないでしょう? ストリーム API には、すっきりとしたコードが書けるような細工が組み込まれています。 ストリームを閉じるべき場所で閉じていない場合は、 ウェブサーバーのエラーログに有用なデバッグ情報が出力されます。

> 注意: 拡張モジュールの開発中は、常にデバッグビルド版の PHP を使用するようにしましょう (configure の際に --enable-debug を指定します)。 そうすることで、メモリリークやストリームのリークに関する重要な警告を受け取れるようになります。

時には、リクエストを持続させるためにストリームをオープンし続けることが有用なこともあるでしょう。 例えばログを記録したり結果をファイルにトレースする場合などです。 このようなストリームについて、確実に後始末を行うコードを書くことはさほど難しくありません。 しかしその数行のコードがどうしても必要なのかというと、そうではないでしょう。 このような場合にコードを書く手間を省くため、ストリームに対して 「このストリームの後始末は自動処理にまかせる」という印をつけることができます。 こうすると、ストリームの後始末が自動的に行われた際に、 ストリーム API は何の警告も発しなくなります。この印をつけるには php_stream_auto_cleanup() を使用します。

### ストリームをオープンする際のオプション

これらの定数は、ストリームファクトリ関数の操作に影響を及ぼします。

`IGNORE_PATH`
  これはストリームのデフォルトのオプションです。要求されたファイルに ついて、include_path に列挙されたパスを検索しないようにします。
`USE_PATH`
  要求されたファイルについて、include_path で列挙されたパスも検索します。
`IGNORE_URL`
  ストリームを開く際に、登録された URL ラッパーの存在を無視します。 非 URL ラッパーについては考慮され、これらがパスをデコードします。 このフラグの逆はありません; ストリームAPIは登録されたすべての ラッパーをデフォルトで使用しようとします。
`IGNORE_URL_WIN`
  Windows においては、IGNORE_URL と等価です。 他のシステムでは効果はありません。
`ENFORCE_SAFE_MODE`
  ファイルを開く前に、ストリームの背後の実装が safe_mode チェックを ファイルに対して行うよう指示します。 このフラグを省くと、safe_mode チェックが行われず、PHP プロセスが アクセス権をもつすべてのファイルに対してオープンが可能になります。
`REPORT_ERRORS`
  このフラグがセットされていて、ファイルまたは URL を開く際に何らかのエラーが 発生した場合に、ストリーム API は php_error 関数をあなたの代わりに 実行します。これは、パスや URL がユーザー名やパスワードなど エラー時にブラウザに表示されるべきでない情報を含むときに (それがセキュリティ上のリスクになるため) 有用です。 ストリーム API がエラーを出すときは、まずユーザー名やパスワードといった 情報をパスから取り除いた後で、エラーメッセージを安全な形にしてから エラーを出力します。
`STREAM_MUST_SEEK`
  このフラグは、拡張モジュール内で、本当にストリームをランダムにシーク する必要があるときに有用です。 いくつかの種類のストリームはそのままの形ではシークができないことが あるので、このフラグをセットしておくと、ストリーム API は、 まず開かれたストリームがシーク可能かどうかを調べ、シークできない場合は、 ストリームのデータをシーク可能な一時的なストレージ (テンポラリファイル あるいはメモリーストリーム) にコピーします。 このフラグは、ストリームに対してシーク動作を行ってから書き込みを行うような 場合には適していません。アクセス対象のストリームは当初アクセスを要求した リソースに必ずしも関連付けられているとは限らないからです。

  > 注意: もし、要求されたリソースがネットワークベースであった場合、 この関数は、すべてのデータが読み込まれるまでブロックします。

`STREAM_WILL_CAST`
  もしあなたの拡張モジュールがサードパーティのライブラリを利用していて、 そのライブラリには FILE* かファイルディスクリプタを渡さなくては ならないとき、このフラグを使うと、ストリーム API にリソースを オープンしても、バッファリングは行わないよう指示することができます。 その後、php_stream_cast() を使い、そのライブラリの 必要とする FILE* や ファイルディスクリプタを取得できます。 このフラグは HTTP URL にアクセスしたとき、 実際のストリームのデータが不定なオフセットの先から始まるような 場合において、特に有用です。 このオプションはストリーム API のレベルでのバッファリングを 無効にするため、ストリーム関数のパフォーマンスが悪くなるかも しれませんが、このオプションを利用するということは、あなたが ストリームの背後にある実装に合わせるよう ストリーム API 関数を使うことをストリームに宣言したということ ですから、それは許容範囲内と考えられます。 このオプションは本当に必要だと確信があるときにのみ用いてください。