PHPのストリームでズンドコキヨシ

[ズンドコキヨシまとめ - Qiita](http://qiita.com/shunsugai@github/items/971a15461de29563bf90)

機を逃した感じですが、ストリームを使った実装はまだ見かけないのでやってみました。

## ストリームって何

PHPにおけるストリームは長大なバイナリを扱うための機構です。似たような仕組みにイテレータやジェネレータがありますが、ストリームはバイナリ列に特化していて、ファイルシステムを偽装できたり、ソケット通信と組み合わせられたりと独特の機能を持ちます。

まあ、C言語のストリームとほとんど同じですね。インターフェースもCを踏襲していて、node.jsのそれのようなAPIではありません。

## ストリームを使ったズンドコキヨシ案

1. ズンとドコを無限に出力するストリームを生成する
2. ストリームフィルタを使って出力を監視し、条件が整ったらキヨシを出力に追加する
3. キヨシの後は何も出力しないようにして、ループを終了させる。

こんな感じでしょうか。無意味に複雑ですがストリームの勉強にはちょうどいいと思います。

### 無限ズンドコストリームの作成

PHP側でストリームを実装するならstreamWrapperを使います。
http://jp2.php.net/manual/ja/class.streamwrapper.php
ここにあるメソッドをひたすら実装していくことになるのですが、今回は読み出しさえできればいいので可能な限り実装を省略します。

```php
class Zundoko
{
    function stream_open($path, $mode, $options, &$opened_path) {
        return true;
    }
    function stream_read(int $count) {
        return (mt_rand(0, 1) ? 'ズン' : 'ドコ') . PHP_EOL;
    }
    function stream_eof() {
        return false;
    }
}
stream_wrapper_register('zundoko', 'Zundoko');
```

これで`zundoko://`というスキーマがZundokoストリームと紐付けられました。fopenすることでzundokoストリームを作成できます。

```php
$zd = fopen('zundoko://', 'r');
for ($i = 0; $i < 10; ++$i) {
    echo fgets($zd);
}
```

実行すると10回`ズン`と`ドコ`が表示されたはず。無限ループにすれば無限にズンドコ出来ます。

file_get_contentsで長さを指定してズンドコ文字列を取得することも出来ます。byte長を指定しないといけないので、欲しいズンドコ数に対して7を掛ける必要があります。

```php
echo file_get_contents('zundoko://', false, null, 0, 70); //ズンドコ10個出力
```

### キヨシストリームフィルタの作成

ストリームを加工するには別のストリームを作って連結すればいいんですが、ストリームフィルタという機構があるので無駄にこれを使ってみることにします。

ストリームフィルタはストリームに設定することでデータ変換を行う機構です。
単純なもので言えばrot13変換を行う`string.rot13`などがあります。php_user_filterという組み込みクラスをextendsすることで作成できます。

```php
class Kiyoshi extends php_user_filter
{
    private $zundoko = [];
    private $finished = false;
    const KIYOSHI = ['ズン', 'ズン', 'ズン', 'ズン', 'ドコ'];

    public function filter($in, $out, &$consumed, $closing)
    {
        if ($this->finished) {
            while ($bucket = stream_bucket_make_writeable($in));
            return PSFS_ERR_FATAL;
        }
        while ($bucket = stream_bucket_make_writeable($in)) {
            $this->zundoko[] = rtrim($bucket->data);
            if (count($this->zundoko) > 5) {
                array_shift($this->zundoko);
            }
            if (self::KIYOSHI === $this->zundoko) {
                $bucket->data    .= "キ・ヨ・シ！\n";
                $bucket->datalen += 19;
                $this->finished = true;
            }
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}
stream_filter_register('kiyoshi', 'Kiyoshi');
```

php_user_filterで実装するのは、filterというメソッドです。
filterに渡される`$in`と`$out`はBucket Brigadeと呼ばれる特殊なリソースで、ストリームが触っているバイナリ列の配列のようなものです。一つのバイナリ列に対応するのがBucketで、Bucketが複数入っているのがBucket Brigadeという感じ。

stream_bucket_make_writeable関数を作用させると一つ一つのBucketを取り出すことが出来ます。

```php
$bucket = stream_bucket_make_writeable($in);
```

var_dumpするとわかりますが、`$bucket`は単純なstdClassオブジェクトです。

```php
class stdClass#3 (3) {
  public $bucket =>
  resource(68) of type (userfilter.bucket)
  public $data =>
  string(7) "ドコ
"
  public $datalen =>
  int(7)
}
```

filterメソッドでは渡された`$in`というBucket BrigadeからBucketを１つずつ取り出し、加工し、`$out`に`stream_bucket_append`で投げ渡せばよい、という感じです。

### 完成版

```php
<?php
// streamでズンドコキヨシ
class Zundoko
{
    function stream_open($path, $mode, $options, &$opened_path) {
        return true;
    }
    function stream_read(int $count) {
        return (mt_rand(0, 1) ? 'ズン' : 'ドコ') . PHP_EOL;
    }
    function stream_eof() {
        return false;
    }
}
stream_wrapper_register('zundoko', 'Zundoko');

class Kiyoshi extends php_user_filter
{
    private $zundoko = [];
    private $finished = false;
    const KIYOSHI = ['ズン', 'ズン', 'ズン', 'ズン', 'ドコ'];
    public function filter($in, $out, &$consumed, $closing)
    {
        if ($this->finished) {
            while ($bucket = stream_bucket_make_writeable($in));
            return PSFS_ERR_FATAL;
        }
        while ($bucket = stream_bucket_make_writeable($in)) {
            $this->zundoko[] = rtrim($bucket->data);
            if (count($this->zundoko) > 5) {
                array_shift($this->zundoko);
            }
            if (self::KIYOSHI === $this->zundoko) {
                $bucket->data     .= "キ・ヨ・シ！\n";
                $bucket->datalen  += 19;
                $this->finished = true;
            }
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}
stream_filter_register('kiyoshi', 'Kiyoshi');

$zd = fopen('zundoko://', 'r');
stream_filter_append($zd, 'kiyoshi');
while ($result = fgets($zd)) {
    echo $result;
}
```

しかし無意味にストリームフィルタなんぞ使わずに、Zundokoストリームでキヨシ判定までやってしまった方が単純かなー。