hnwの日記

見直されるべきPHP5の組み込みイテレータ

PHPには5.0.0以降SPL (Standard PHP Libray)という枠組みが導入されています。これにより、Iteratorインターフェースを実装したクラスのインスタンスであれば、foreach文で配列と同じように取り扱えます。自分でクラスを作るときもIteratorを実装すれば使うのが楽ですし、コードも読みやすくなると思います。


また、PHPに標準で組み込まれているクラスにはIteratorを実装しているものが多数あります。たとえば僕の手元のPHP5.2.9には24個のイテレータがあり、そのうちいくつかは十分に実用的なクラスです。ただ、日本語の資料が少ないせいか、かなり知名度は低いように思います。本記事では4つの便利な組み込みイテレータを紹介します。


SPLのクラスにはデザインパターンの考えが多く含まれています。特に、イテレータを元にイテレータを作るような使い方は、保守性の高いシンプルなコードを書く上でヒントになると思います。

SplFileObject:ファイルの内容を1行づつ返すイテレータ

SplFileObjectというのは、本来はファイル操作関数の代わりにオブジェクト指向風な操作を提供するクラスで、PHP5.1.0以降で使えます。今回はイテレータとして使ったときの機能に注目してみます。

<?php
$inputs = new SplFileObject(__FILE__, 'r');
foreach($inputs as $line) {
  print $line;
}


このイテレータは、ファイルの内容を1行づつ返します。これを使えばPerlのようなノリでフィルタプログラムが書けそうですよね。

NoRewindIterator:Rewindできないイテレータをforeachで使う

ところで、SplFileObjectを実際に使ってみると、一つ問題点に気づきます。'php://stdin'に対してイテレータがうまく働かないことがあるのです。

<?php
$inputs = new SplFileObject("php://stdin");
foreach($inputs as $line) {
  print $line;
}


上記プログラムは下記のような例外を発生します。

$ echo hoge | php stdin-rewind-failure.php
PHP Warning:  SplFileObject::rewind(): stream does not support seeking in /tmp/stdin-rewind-failure.php on line 3
PHP Fatal error:  Uncaught exception 'RuntimeException' with message 'Cannot rewind file php://stdin' in /tmp/stdin-rewind-failure.php:3
Stack trace:
#0 /tmp/stdin-rewind-failure.php(3): SplFileObject->rewind()
#1 {main}
  thrown in /tmp/stdin-rewind-failure.php on line 3


これはどういうことでしょうか。イテレータをforeachで使うと最初に必ずrewind()が呼ばれます。しかし、標準入力には実体ファイルがあるわけではありません。rewind()でファイル先頭に巻き戻すことが不可能ということで、例外が発生しているわけです。


こんなときにNoRewindIteratorが利用できます。これは、既存のイテレータのrewindを呼ばないようにするイテレータです。

<?php
$inputs = new NoRewindIterator(new SplFileObject("php://stdin"));
foreach($inputs as $line) {
  print $line;
}


NoRewindIteratorを利用することで、標準入力もファイル同様にイテレータとして扱えるようになりました。

AppendIterator:複数のイテレータを結合する

AppendIteratorも既存のイテレータの挙動を変更するイテレータで、複数のイテレータを一つのイテレータのように扱えます。具体的には、1つ目のイテレータの最後の要素まで到達すると2つ目のイテレータの先頭要素を返します。

<?php
$inputs = new AppendIterator();
$inputs->append(new SplFileObject('example1.txt', 'r'));
$inputs->append(new SplFileObject('example2.txt', 'r'));

foreach($inputs as $line) {
  print $line;
}


このように、複数ファイルの内容を連結して1つのイテレータとして処理したい、などというときにはSplFileObjectを2つ作り、AppendIteratorで一つのイテレータとして扱うことができます。

This is Example1.
HELLO
12345
bye
This is Example2.
678foo
0


このように、「cat example1.txt example2.txt」と同じ結果が得られました。

RegexIterator:正規表現マッチを行うイテレータ

既存のイテレータから新しいイテレータを作る、という考えに慣れてきたのではないでしょうか。最後に紹介するイテレータRegexIteratorです。これは、既存のイテレータから条件に合う要素だけを取り出すイテレータです。

<?php
$inputs = new AppendIterator();
$inputs->append(new SplFileObject('example1.txt', 'r'));
$inputs->append(new SplFileObject('example2.txt', 'r'));
$filtered_inputs = new RegexIterator($inputs, '/^\d*$/');

foreach($filtered_inputs as $line) {
  print $line;
}


先ほど例として使ったAppendIteratorにRegexIteratorを適用しました。

12345
0


結果は上記のように、2ファイルの内容のうち、正規表現にマッチする行だけが表示されます。AppendIteratorを元に、正規表現にマッチする行だけを返すようなイテレータが作られたわけです。


これはコマンドをパイプでつなぐのに近い感覚で捉えられると思います。上の結果は「cat example1.txt example2.txt | grep '^[0-9]*$'」と同じですよね。

まとめ


組み込みイテレータの中には、他にも気になるクラスやメソッドがたくさんあります。他に面白いものを見つけた人は是非教えてください。また、多数のクラスやインターフェースが関係しているので、これはデザインパターンで言うと何かな、と考えるのも面白いと思います。


最後に、SPLの概要を把握する上で非常に優れた資料が公開されていますので、紹介します。2007年12月に開催されたPiece Network 1で関山さんが発表された「SPL入門 - SPLで学ぶPHP5のオブジェクト指向 -」です。本記事の4つのイテレータを含め網羅的な解説がありますので、PHP使いなら一度は見ておくべきだと思います。