配列ポインタ操作関数にオブジェクトを突っ込む
やたらたくさんあるPHPの配列関数ですが、大半は配列の全ての値に対して一律に処理を行う関数です。
キーと値を入れ替えるarray_flip、特定のカラムを取り出すarray_column、任意の関数を適用するarray_walkなどですね。
ところでこの中に一部、配列のポインタを直接触る関数群があります。
キーを取得するkey、値を取得するcurrent、ポインタをひとつ進めるnext、ポインタを先頭に戻すresetなどです。
よく見るとこれらの関数、シグネチャがarray|object &$array
となっていて、実は配列以外にオブジェクトも反復動作させることができます。
となればIteratorを使って操作したくなるわけですが、ここには罠が存在します。
class A implements IteratorAggregate{
public $a = 1;
public $b = 2;
public function getIterator() {
return new ArrayIterator([3, 4]);
}
}
// foreach
$a = new A();
foreach ($a as $v) {
var_dump($v); // 3, 4
}
// current
while ($v = current($a)) {
var_dump($v); // 1, 2
next($a);
}
なんかIterator無視するんですけど。
IteratorはPHPの機能のひとつで、オブジェクトの反復動作を任意に設定することができる機能です。
本来はオブジェクトをforeach
すると可視のプロパティが順に出てくるのですが、それを異なる動作にすることができます。
のですが、配列ポインタ操作関数は反復動作とは関係ないので動作に影響しないというわけですね。
一見同じような機能なのに動作が違うということで、非常にわかりにくい挙動となっています。
しかも、このあたりの話はマニュアルにも全く書かれていません。
僅かにforeachに書かれている『foreach は、 current() や key() のような関数で使われる、内部的な配列のポインタを変更しない点に注意して下さい。』がほぼ唯一の言及ですが、これの一文だけでIteratorが効かないとまで解釈しきれる人はそう多くないでしょう。
対策としては、そもそも今どき配列ポインタ操作関数を自力で使わないのが一番です。
ループ制御はオブジェクトだろうが配列だろうがforeach一択です。
ちなみにArrayObjectを継承すると、getIterator
が存在するかどうかでforeachとnextの出力がかわります。
益々わけがわからない。
class A extends ArrayObject{}
// foreach
$a = new A([1, 2]);
foreach ($a as $v) {
var_dump($v); // 1, 2
}
// current
while ($v = current($a)) {
var_dump($v); // 何も出力されない
next($a);
}
class B extends ArrayObject{
public function getIterator() {
return new ArrayIterator([3, 4]);
}
}
// foreach
$b = new B([1, 2]);
foreach ($b as $v) {
var_dump($v); // 何も出力されない
}
// current
while ($v = current($b)) {
var_dump($v); // 3, 4
next($b);
}
PHP8.1
この動作は非常にわかりにくいということで、PHP8.1以降は配列ポインタ操作関数にオブジェクトを突っ込めないようになります。
PHP8.1でE_DEPRECATED、PHP9でエラーになる予定です。
$a = new A([1, 2]);
while ($v = current($a)) {
var_dump($v);
next($a);
}
// PHP8.0まで エラー無し
// PHP8.1以降 E_DEPRECATED
// PHP9移行 エラー