array_walk_recursive
の Generator 版のようなものです。
処理される Iterator は次のように Generator で実装することにします。
<?php
return call_user_func(function () {
yield 1;
yield 2;
yield call_user_func(function () {
yield 3;
yield 4;
yield call_user_func(function () {
yield 5;
yield 6;
});
});
});
とりあえず普通に array_walk_recursive
っぽいものを作ってみます。
<?php
$recursive = function ($arr, callable $callback) use (&$recursive) {
foreach ($arr as $key => $val) {
if (is_array($val) || $val instanceof \Traversable) {
$recursive($val, $callback);
} else {
$callback($val, $key);
}
}
};
$iterator = include __DIR__ . '/g.php';
$recursive($iterator, function ($val, $key) {
echo "$key => $val,\n";
});
/*
0 => 1,
1 => 2,
0 => 3,
1 => 4,
0 => 5,
1 => 6,
*/
単純に callback のところを yield にすれば大丈夫かなーと思いましたが、そんなことはありません。
<?php
$recursive = function ($arr) use (&$recursive) {
foreach ($arr as $key => $val) {
if (is_array($val) || $val instanceof \Traversable) {
$recursive($val);
} else {
yield $key => $val;
}
}
};
$iterator = include __DIR__ . '/g.php';
foreach ($recursive($iterator) as $key => $val) {
echo "$key => $val\n";
}
/*
0 => 1
1 => 2
*/
$recursive($val)
は Generator を返しているので、呼び出し側でそれを回してやる必要があります。
<?php
$recursive = function ($arr) use (&$recursive) {
foreach ($arr as $key => $val) {
if (is_array($val) || $val instanceof \Traversable) {
foreach ($recursive($val) as $k => $v) {
yield $k => $v;
}
} else {
yield $key => $val;
}
}
};
$iterator = include __DIR__ . '/g.php';
foreach ($recursive($iterator) as $key => $val) {
echo "$key => $val\n";
}
/*
0 => 1
1 => 2
0 => 3
1 => 4
0 => 5
1 => 6
*/
なお、いくつかの Iterator の実装では Iterator の current が Iterator 自身になることがあります、そのような Iterator にこの方法は使えません。無限に再帰してしまいます。
<?php
$iterator = new \DirectoryIterator(__DIR__);
foreach ($iterator as $it) {
var_dump($iterator === $it);
}
/*
bool(true)
bool(true)
bool(true)
*/
再帰の条件を付け足してそのような Iterator が渡されても無限再帰しないようにします。
<?php
$recursive = function ($arr) use (&$recursive) {
foreach ($arr as $key => $val) {
if (is_array($val) || ($val instanceof \Traversable && $arr !== $val)) {
foreach ($recursive($val) as $k => $v) {
yield $k => $v;
}
} else {
yield $key => $val;
}
}
};
$iterator = new \DirectoryIterator(__DIR__);
foreach ($recursive($iterator) as $key => $val) {
echo "$key => $val\n";
}
/*
0 => .
1 => ..
2 => 01.php
3 => 02.php
:
*/
ところで、SPL に再帰的な Iterator をフラットに反復する Iterator があります。RecursiveIteratorIterator です。
RecursiveIteratorIterator は RecursiveIterator をインプリメントした再帰的な Iterator をフラットに反復します。
ただの配列なら RecursiveArrayIterator と組み合わせて次のようにできます。
<?php
$arr = [1, 2, [3, 4, [5, 6]]];
$iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($arr));
foreach ($iterator as $key => $val) {
echo "$key => $val,\n";
}
/*
0 => 1,
1 => 2,
0 => 3,
1 => 4,
0 => 5,
1 => 6,
*/
Iterator を返す Iterator は RecursiveIterator ではないため、これをそのまま使うことはできません。
折角なので RecursiveArrayIterator の Iterator 版を作ってみます(もはや Generator はあまり関係ないですが)。
RecursiveArrayIterator に対して RecursiveIteratorIterator かなーと思ったのですがその名前は使えないので RecursiveRecursiveIterator としました。
IteratorIterator を継承しています。Traversable を Iterator にするためのものですが、既存の Iterator に Decorator 的なことをやるために使えます。
<?php
class RecursiveRecursiveIterator extends \IteratorIterator implements \RecursiveIterator
{
public function hasChildren()
{
return $this->current() instanceof \Traversable && $this !== $this->current();
}
public function getChildren()
{
if ($this->current() instanceof \RecursiveIterator) {
return $this->current();
} else {
return new static($this->current());
}
}
}
$iterator = include __DIR__ . '/g.php';
$recursiveIterator = new \RecursiveIteratorIterator(
new \RecursiveRecursiveIterator($iterator)
);
foreach ($recursiveIterator as $key => $val) {
echo "$key => $val,\n";
}
/*
0 => 1,
1 => 2,
0 => 3,
1 => 4,
0 => 5,
1 => 6,
*/