ryunosuke/stream-wrapper

v1.1.2 2024-08-09 11:51 UTC

This package is auto-updated.

Last update: 2024-11-09 12:34:43 UTC


README

Description

php のストリームラッパーを規約するパッケージです。

php のストリームラッパーは非常に強力かつ柔軟性があるのですが、いかんせん使い方が難しく、ドキュメントを読んでもイマイチ実装方法が分かりません。 また、実装するにしてもインターフェースが切られておらず、ドキュメントと悪戦苦闘して試行錯誤しながら実装する必要があります。 さらにいくつかは(一見関連がなさそうに見えるものの)同じメソッドで引数分岐なものがあり、依存関係が非常に分かりにくいです。

そこで、インターフェースを宣言し、決められたメソッドを実装すればストリームラッパーとして動作する interface と trait をあらかじめ用意しておく、のがこのパッケージの主旨です。

Install

{
  "require": {
    "ryunosuke/stream-wrapper": "dev-master"
  }
}

Feature

ディレクトリ構成

├── Exception/
├── Utils/
├── Stream/
├── Mixin
│   ├── DelegateTrait.php
│   ├── DirectoryIOTrait.php
│   ├── DirectoryIteratorTrait.php
│   ├── StreamTrait.php
│   ├── UrlIOTrait.php
│   └── UrlPermissionTrait.php
├── PhpStreamWrapperInterface.php
├── StreamWrapperAdapterInterface.php
├── StreamWrapperAdapterTrait.php
└── StreamWrapperNoopTrait.php

Top Directory

これがこのパッケージの目的です。他はすべておまけです

PhpStreamWrapperInterface

php オリジナルのストリームラッパーを interface として切り出したものです。

おそらくですが、 php 本体がインターフェースを用意しない理由として一部のメソッドは実装してはならないというものがあると思います。 例えば mkdir( https://www.php.net/manual/streamwrapper.mkdir.php )などは「適切なエラーメッセージを返すためには、ラッパーがディレクトリの作成に対応していない場合にはこのメソッドを定義してはいけません」とあります。他にもいくつか同様の注意書きが書かれたメソッドがあります。 この言葉を信じるならば「適切なエラーメッセージを返すため」程度の話であり、エラーメッセージのためにインターフェースが切れないのはすこぶる不便です。 のでこのパッケージでは例外で対応する、と割り切ることでインターフェースを用意しています。

まぁ実際のところ必要ありません。本家が interface を用意していない以上、別に使わずともエラーにはならないためです。 実行時エラーではなくコンパイル時エラーにしたいため、主に開発時のために用意されています。

StreamWrapperAdapterInterface

本パッケージにおけるストリームラッパーを規約する interface です。

前述の通り、標準ストリームラッパーはメソッド名が分かりにくかったり、妙な場合分けがあったり、唐突に context という概念(しかもリソース)が出てきたりして、使いやすいとは言えないので、この interface で「関数名とメソッド名が一致するようなインターフェース」を切り、それを StreamWrapperAdapterTrait で本家と接続します。 これによって context や場合分けなどはすべて吸収され、簡潔なメソッド群とそれに対応する関数がマップされる構造になります。

StreamWrapperAdapterTrait

StreamWrapperAdapterInterface と PhpStreamWrapperInterface を接続する trait です。

StreamWrapperAdapterInterface は要するにオレオレ規約を設けているだけなのでそれだけではストリームラッパーとして動きません。 動かすためには PhpStreamWrapperInterface の実装とそれらを繋ぐボイラープレートが必要になります。 それを担う trait です。

返り値にユニオン型はほとんど使いません(今のところ readdir だけが特例)。 しかるべき値が返せない場合は ErrorException を投げれば php のエラーに変換されるようになっています。

context という概念は抹消されます。context はパースされて、そのオプション配列として引数で渡ってきます。 context にはさらに options と params という概念があるため、2つの引数を受け取ります。 もっとも、 params に関してはドキュメントが非常に乏しく、使うことはほとんどないでしょう(公式ドキュメントでも stream_notification_callback という形で一例しか使われていません)。

StreamWrapperNoopTrait

StreamWrapperAdapterInterface のメソッドをすべて実装していますが、すべて例外を投げる trait です。

ストリームラッパーをいざ実装しようとすると使わないメソッドまで実装する必要が出てくるため、それを補う意味で全メソッドで呼ぶと例外が発生する実装になっています(一部特例あり)。 これを use して、実装側クラスで必要なものだけを実装すればそのクラスはストリームラッパーとして動作させることができる、ということです。

class HogeStream implements
    PhpStreamWrapperInterface,    // これにより、php オリジナルのストリームラッパーインターフェースがすべて規約されます(なくても動きますが、エラーが実行時になります)
    StreamWrapperAdapterInterface // これにより、オレオレストリームラッパーインターフェースがすべて規約されます
{
    use StreamWrapperAdapterTrait; // オレオレストリームラッパーと php オリジナルのストリームラッパーを接続させるための trait です
    use StreamWrapperNoopTrait;    // すべてが例外を投げるデフォルト実装 trait です

    public function _stat(string $url): array // これを実装すれば stat(filesize や filemtime) を実装したことになります
    {
        // ...
    }
}

下記はメソッドの詳細です。一部改変しています。 全部読むとストリームラッパーのあまりにもカオスな状態が分かると思います。

_mkdir

関数版は引数が個別で分かれていますが、ストリーム版はビットフラグで流れてくるため、バラしてから呼び出します。 つまり、インターフェースとしては関数版 mkdir と同じです(context 以外)。

_rmdir

実装すべきストリーム版に謎の引数 $options がありますが、対応していません。 ドキュメントは存在し、「STREAM_MKDIR_RECURSIVE などの値のビットマスク」とありますが、関数版にそのような引数はありません。 確かに rmdir で再帰的に削除できたら便利な場面もありますが…。これ mkdir のコピペミスなんじゃ…と思っています。

_touch

関数版 tocuh に対応するストリーム版メソッドは存在しません。stream_metadata の引数分岐で実装されています。 「touch の場合は…chmod の場合は…」と細かく書かれていますが、interface+trait ですべて吸収してあります。 つまり、インターフェースとしては関数版 touch とほぼ同じです。 「ほぼ」というのは ?int が int になっています。省略時や両方指定時の値はすべて解決されて渡ってきます。

余談ですが、命名規則がおかしく、引数はパスなので stream_metadata ではなく url_metadata の方が正しいと思ってます。 (「ストリームラッパーの関数だから」ということなんでしょうが…。でもだとすると utl_stat の説明がつかない)。

_chmod

関数版 chmod に対応するストリーム関数は存在しません。stream_metadata の引数分岐で実装されています。 つまり、すべてにおいて touch の説明と同じです。

_chown

関数版 chown に対応するストリーム関数は存在しません。stream_metadata の引数分岐で実装されています。 つまり、すべてにおいて touch の説明と同じですが、名前引きされてからコールされるので $uid は int です。

_chgrp

関数版 chgrp に対応するストリーム関数は存在しません。stream_metadata の引数分岐で実装されています。 つまり、すべてにおいて touch の説明と同じですが、名前引きされてからコールされるので $gid は int です。

_unlink

特に変わり映えしません。

_rename

特に変わり映えしません。

_stat

特に変わり映えしません。 なお、stat なので仕方ありませんが、このメソッドに対応する関数は非常に多岐に渡ります。 大抵のカスタムラッパーで実装は必須になるでしょう(最低限 size だけでも返す方が望ましい)。

_lstat

対応するストリーム側のメソッドはありません。リンクという概念はファイルシステムの影響が強いため、ストリームラッパーとしてコールされることはほぼないのでしょう。 本パッケージでは関数と1対1に対応させたかったため、敢えて分けています。 のでこのメソッドは特別扱いで、コールすると「未実装例外」ではなくデフォルトで _stat へ委譲されます。

_opendir

実装すべきストリーム版に謎の引数 $options がありますが、対応していません。 ドキュメントも存在せず、何のための引数かも不明なためです。 最初は scandir の $sorting_order 引数に対応するかと思ったんですが、どうもそんなこともないようです。

この opendir と fopen はストリームを開く関数のため、内部状態を持たせる必要があります。 本パッケージでは状態を持つことを嫌って、引数で取り回しをしています。 つまり、_opendir で何らかのオブジェクトを返すと、そのオブジェクトが _readdir, _closedir の引数として渡ってきます。 この仕様によって、開いた何かをプロパティなどで保持する処理を不要としています。 使用する際も「fopen で開いたリソースを fread 等に渡す」という使い方のため、この仕様の方が直感的だと思っています。 StreamWrapper を素で実装する際、「fread に引数がないけど fopen したリソースはどうやって取得するの?」となりがちだったのです。

_readdir

$dir_handle の省略は不要なので対応していません。 php によくある「引数は省略可能で、省略した場合は最後のリソースが使われます」は昔の名残であり、現在ではほとんど不要な仕様だと思います。

あとオリジナルは string|false です(※ ドキュメントは string ですが誤りです)が、回しきった場合は null を返します。 これは ?string と表現できる、という一点の理由のみです。 php8 に移行してユニオン型が使えるようになったら array|false にするかもしれませんが(個人的な理由で)かなり先の話です。

_rewinddir

特に変わり映えしません。

_closedir

特に変わり映えしません。

_fopen

一部謎の引数がありますが、すべて吸収してあるので、インターフェースとしては fopen とほぼ同じです。 返り値については _opendir と同じことが言え、この _fopen で返したオブジェクトが _fread_fwrite 等のメソッドの引数として渡ってきます。

下記は改変部分です。

まず $options ですが、STREAM_USE_PATH は字のごとくです。foepnfile_get_contents の第3引数に対応します。 …が、このパッケージではほぼ対応しておらず、引数も渡しません。 というのも、カスタムストリームラッパーの特性上、$path はすべてスキームから始まるフルパスであり、相対パスが流れてくることがありません。 また、インクルードパスは file スキームを前提に考えられており、カスタムストリームの相対パスがインクルードパスに設定されているという状況自体がまずあり得ません。 (超限定的な状況で fopen がスキームなしで呼ばれることはあるため STREAM_USE_PATH はそのための機能なのかもしれませんが、ここでは割愛します)。

次に STREAM_REPORT_ERRORS ですが、これが立って渡ってくる状況をほとんど見つけることができませんでした。 一応は対応しており、このフラグが立ってないと警告は抑止されるようになってます(ので fopen 自体のエラーは報告されるが、詳細なエラーが分からなくなります)。 php-src を確認したところ、内部的に REPORT_ERRORS はそこそこ使用されているようですが、「別の目的のためにファイルを読み込む必要がある場合(≒ファイルの open が主目的でない場合)」に抑制されている傾向があるようでした。 例えば finfo や exif 等です。 これに関しては非常に不便なため常にエラー出力が為されるように修正される可能性があります。

$opened_path はほぼ未対応です。 これはインクルードパスから見つけた場合のフルパスのレシーバ引数ですが、上記の通りその状況自体がほぼ存在しないためです。

_fread

特に変わり映えしません。 敢えて言うならドキュメントの注意書きが多いので要注意といったところでしょうか。

  • 戻り値が count より長い場合は E_WARNING エラーが発生し、余分なデータは失われます。
  • streamWrapper::stream_eof() は、 streamWrapper::stream_read() がコールされた後に直接コールされ、 EOF に達したかどうかを調べます。実装されていない場合は EOF だとみなされます。
  • ファイル全体を (file_get_contents() などで) 読み込む場合、PHP はループ内で streamWrapper::stream_read() をコールしてから streamWrapper::stream_eof() をコールします。 しかし、streamWrapper::stream_read() が空でない文字列を返す限りは streamWrapper::stream_eof() の戻り値を無視します。

これは php の仕様であり、パッケージ側でどうにかできる話ではないため引用に留めます。

_fwrite

特に変わり映えしません。

_ftruncate

特に変わり映えしません。

_fclose

特に変わり映えしません。 敢えて言うなら戻り値を bool にしています。 これは AWS も嘆いているんですが、もしかしたら将来変更されるかも…と淡い期待を抱いて bool としています。

_ftell

特に変わり映えしません。 敢えて言うならドキュメントでは「このメソッドは、fseek() に対応してコールされ、現在の位置を決定します」とありますが、これは間違いではありません。 php のメーリングリスト(URL 失念)を辿った結果、深遠な理由でこうなっていてこう書かれているようなのでこれが正となります。 これに関しては seek の方で少し触れます。

_fseek

特に変わり映えしません。 敢えて言うならドキュメントの「現在の実装は、 whence の値を SEEK_CUR に設定することはありません。 そのようなシークは、 内部的に SEEK_SET と同じ動きに変換されます」は誤りです。 確認したところ少なくとも Windows ではファイルフラグ "a" で fopen すると SEEK_CUR でコールされることが確認できました。 つまり SEEK_CUR の実装の必要があるということです。

ちなみに _ftell で触れた tell と seek の件は下記が参考になるでしょう。

  • 成功した場合、 streamWrapper::stream_seek() をコールした直後に streamWrapper::stream_tell() がコールされます。 streamWrapper::stream_tell() が失敗すると、 呼び出し元関数への戻り値は false に設定されます。
_feof

特に変わり映えしません。

_fflush

特に変わり映えしません。

_flock

「ロックがブロックされた」場合のレシーバ引数 $would_block は対応していません。 ストリーム側に対応する引数が存在しないため、呼び元に値を伝える術がないためです。

_fstat

特に変わり映えしません。

_stream_set_blocking

これも touch などと同様、引数分岐なので個別メソッドに分けてそれらがコールされるようになっています。 それ以外は特に変わり映えしません。

_stream_set_read_buffer

これも touch などと同様、引数分岐なので個別メソッドに分けてそれらがコールされるようになっています。 それ以外は特に変わり映えしません。 敢えて言うならばドキュメントは ReadBuffer に一切の言及がないのですが、きちんと呼ばれるようです(おそらくドキュメントの誤り?)。 また、返り値が特殊で「成功時に 0 を、要求通りに設定できなかった場合はそれ以外の値」です。 大抵の場合はエラーメッセージで判別がつくので、bool に寄せています。

_stream_set_write_buffer

これも touch などと同様、引数分岐なので個別メソッドに分けてそれらがコールされるようになっています。 それ以外は特に変わり映えしません。_stream_set_read_buffer と同じです。

_stream_set_timeout

これも touch などと同様、引数分岐なので個別メソッドに分けてそれらがコールされるようになっています。 関数版ではタイムアウト指定は $seconds+$microseconds ですが、分かりにくいので float にしてまとめています(完全に個人の好みです)。 つまり 2.5 秒に設定したい場合は (2, 500000) ではなく (2.5) と指定します。 これは元の syscall が剥き出しになっているだけだと思いますが、そこまでの精度は大抵の場合必要ないでしょう。

なお、このメソッドを実装する必要はほとんどありません。 タイムアウトを設定し、その値でもってタイムアウトを実装することは可能ですが、それを呼び元に伝える術(stream_get_meta_data の返り値)が提供されていないためです。 stream_read に &$timedout のようなレシーバ引数があれば実現できるのですが…。 ちなみに組み込みラッパーである http でもタイムアウトは実装されていないようです(http のタイムアウトはコンテキストオプションで指定します)。

_stream_select

関数版 stream_select 関数はそこそこ良く使うのですが、ストリーム版 stream_cast でどうマッピングすればいいのか情報が足りず分からないため、この関数は特別扱いで「未実装例外」は投げずに return false でデフォルト実装されています。 と、いうのも、使わなければ実装しなければいいだけの話なんですが、どうも mime_content_type を呼ぶと stream_select(cast) がコールされるようです。 実際は false 返しで動作はするようなのでそのようにしています。

対応表

下記が対応表です。 幅の関係上、型やデフォルト値は省いてあります。

Mixin

実際のところストリームラッパーは単体を実装すればいいというわけではなく、複数のメソッドが相互に関連します。 例えば file_get_contents を呼ぶと fopen fread feof など様々なメソッドがコールされます。 意外なところでは include/require を呼ぶと stream_set_read_buffer もコールされます。 あとは上記で挙げた mime_content_type での stream_cast も意外性が高いです。 これらをすべてをいちいち実装してはいられないし、大抵のラッパーでは実装がほぼ同じになるので、それをあらかじめ定義した trait 群です。

StreamTrait

ストリーム(リソース)を読み書きする用の trait です。 内部的にバッファリングします。バッファリングしないストリームラッパーというものは考えにくく、大抵のラッパーではバッファリングが必要になるはずです。 その時、これさえ use しておけばすべてのストリーム操作は完備されます。 所詮 trait なので対応できない場合は個別でメソッドを定義すればそちらが使われます。

なお、一般に想起される「バッファー」とは少し毛色が異なります。 端的に言えば「open 時にまるっと全部読んできて flush 時にまるっと全部書き込む」バッファーです。 IO を本当の意味で「バッファリング」して部分的に書き換えるのは実装難度の割に有用性が低く、大抵のユースケースでは不要のためそのような実装になっています。

DirectoryIOTrait

ディレクトリをサポートするスキーム用の trait です。 mkdir/rmdir の2種類(敢えて言うなら rename/stat も)しか対応する関数が存在しないですが、mkdir は再帰的処理、rmdir は中身がある場合に消せない、という共通処理があるので trait で切り出しています。 特に特筆することはありません。素直な実装になっています。

DirectoryIteratorTrait

scandir 用の trait です。 scandir のためには opendir,readdir,rewinddir,closedir という4つのメソッドを実装する必要がありますが、IteratorAggregate や Generator, iterable などが用意された現代でこの4つを実装する必要はありません。 Iterator を1つ与えてあげればすべての実装は完備されるはずです。そのための trait です。 scandir だけなら問題ありませんが、直に rewinddir を呼ぶ場合は rewindable な Iterator を渡す必要があります。

なお、 DirectoryIOTrait とは相関しません。 ディレクトリという概念が存在せずとも「"/" を目印にディレクトリのように探索」は実装可能です(S3 がいい例でしょう)。

UrlIOTrait

ストリームではなく、パスベースでの関数用の入出力(filesize, rename, unlink) trait です。 特に特筆することはありません。素直な実装になっています。

UrlPermissionTrait

ストリームではなく、パスベースでの関数用のパーミッション(chmod, chown, chgrp) trait です。 権限制御は行われません。誰でもいつでも変更することができます。 理由は下記の実装がめんどくさかっただけです。

  • chmod: たいていのシステムでは、ファイルの所有者のみがそのモードを変更可能です
  • chown: スーパーユーザーのみがファイルの所有者を変更できます
  • chgrp: スーパーユーザーのみがファイルのグループを任意に変更できます。その他のユーザーは、ファイルのグループをそのユーザーがメンバーとして属しているグループに変更できます
暗黙の共通 API

すべての trait に共通して暗黙の API のようなものがあります。コード上は abstract で表現されています。 例えば書き込み処理は UrlIOTrait でも StreamTrait でも使用されますし、存在確認などはほぼすべての trait で使用されます。 それらを各々実装するのは面倒なので、意図的に命名を揃えることで一部の trait で実装したメソッドを他の関数でも流用できるようにしています。 現在のところ下記のメソッドがあります。

function parent(): ?string;
function children(...): iterable;
function move(...): void;

URL 操作系メソッドです。 ファイル・ディレクトリを問わずコールされる可能性があります。

parent, children はそれぞれ 親URL(string) 子URL(iterable) を返す必要があります。

function getMetadata(...): ?array;
function setMetadata(...): void;

メタデータ操作系メソッドです。 ファイル・ディレクトリを問わずコールされる可能性があります。

getMetadata は存在チェックも兼ねます。 エントリが存在しない場合は null を返さなければなりません。

function createDirectory(...): void;
function deleteDirectory(...): void;

ディレクトリ操作系メソッドです。 前述の通り、ディレクトリをサポートするにしても最低限 mkdir と rmdir を実装すれば事足ります。 ただし mkdir には再帰オプションがあり、rmdir には空チェックの必要があります。 それらを汎化するためにこれらのメソッドが必要になります。 標準関数と合わせる必要がない、あるいは常に再帰的動作をするならこれらのメソッド(trait)を使用する必要はありません。 場合によってはそっちの方が利便性が高いでしょう。

function selectFile(...): string;
function createFile(...): void;
function appendFile(...): void;
function deleteFile(...): void;

ファイル操作系メソッドです。

selectFile は $metadata が格納される参照引数があります。 getMetadata と同じ配列が格納される必要があります。 わざわざ参照引数があるのは1回の操作でメタデータとコンテンツの両方を取得できる場合に勿体ないからです(selectFile+getMetadata がアトミックに行える場合は特に)。

createFile は $metadata が格納される引数があります。 setMetadata と同じ配列が格納される必要があります。 わざわざ引数があるのは1回の操作でメタデータとコンテンツの両方を更新できる場合に勿体ないからです(createFile+setMetadata がアトミックに行える場合は特に)。

appendFile は "a" モードで開いた場合の追記処理です。 まるっと引っ張ってきてまるっと保存でもいいんですが、ものによっては専用処理で末尾追加ができたりする(mysql の CONCAT, redis の APPEND 等)ので、使用しないと効率が全く違ってきます。 もっと言うと末尾追加ができない場合、そもそも "a" モードを使うべきではありません。

Stream

Stream にある○○Stream はあたかも本パッケージの主役であるような顔をしていますが、すべてリファレンス実装です。 作者が実装の多様性のために思いついたまま実装して「なんとなく使えそう」と至ったものが配置されているだけです。 そのまま使えないこともないですが、決め打ち実装も多く、凝ったことは全くできません。 繰り返しになりますが、本パッケージの主役はトップディレクトリの interface+trait です

ただ一応思想のようなものはあるので紹介しておきます。

scheme://hostname:port/path/to/?query#file.json
───   ──────  ──── ──  ────
  │          │           │    │      └─  プロトコルにおける「キー名」です
  │          │           │    └─────  プロトコルの「パラメータ」です
  │          │           └────────  プロトコルにおける「内部的な位置」を示します
  │          └─────────────── プロトコルにおける「ネットワーク上の位置」を示します
  └───────────────────── プロトコル名です

例えば mysql だとすると

  • ネットワーク上の位置: DSN に相当します
  • 内部的な位置: スキーマ・テーブル名・主キーに相当します
  • パラメータ: charset などに相当します
  • キー名: (対応しているなら)主キーに相当します

例としては mysql://127.0.0.1:3306/dbname/tablename?charset=utf8mb4#pkval となります。こう記述すると分かりやすいでしょう。 他に redis であれば redis://127.0.0.1:6379/dbindex/key となり、 S3 であれば s3://endpoint/bucket/objectname となります。 組み込みの zip スキームは fragment で内部ファイルを表すため zip:///path/to/file.zip#localname.txt のようになります。

このように「URL で KVS 的な規約を設け、あらゆるプロトコルでストリームラッパーを KVS 的に使えるようにする」が目的としてありました。 ただし実際のところは上記の通り実装の多様性のためのリファレンス実装です。

そもそも mysql であれば PDO や Doctrine があるのでストリームラッパーを使用する必要性は皆無ですし、 S3 は AWS 謹製のストリームラッパー実装があります。 下記のような狙いで実装されています。

  • 全部: interface+trait だけだとテストが書けず、「本当にこれでよいのか?」が不明瞭のため実際のストリームを実装する必要があった(実際、実装していく過程で多様性が生まれた)
  • Array: 高速なテスト+file スキームとの完全互換のため
  • Mysql: スキーマフルのため+flockのため
  • php: 個人的な用途のため
  • Redis: スキーマレスのため+TTL(context)のため
  • S3: 疑似ディレクトリサポートのため
  • Smtp: write only なストリームがあったら面白いと思ったため(ほぼ思いつき)
  • Zip: 特殊なアクセス(fragment が内部ファイル名)のため

License

MIT

Release

バージョニングは romantic versioning に準拠します(semantic versioning ではありません)。

  • メジャー: 大規模な互換性破壊の際にアップします(アーキテクチャ、クラス構造の変更など)
  • マイナー: 小規模な互換性破壊の際にアップします(引数の変更、タイプヒントの追加など)
  • パッチ: 互換性破壊はありません(デフォルト引数の追加や、新たなクラスの追加、コードフォーマットなど)

1.1.2

  • [feature] php8.2 のエラーを修正
  • [fixbug] 拡張子がない場合でもドットが付いてしまう不具合

1.1.1

  • [feature] 拡張子などの付随情報を付与できる php プロトコル
  • [feature] 標準プロトコルへの委譲 trait
  • [feature] URL のローカルプロトコル対応
  • [feature] URL の細分化

1.1.0

  • [*change] flock のシンプル化

1.0.0

  • 公開