RecursiveIterator / RecursiveArrayIterator / RecursiveIteratorIterator
概述
PHP SPL 提供了一组递归迭代器,用于遍历嵌套数据结构。RecursiveIterator 接口定义了递归迭代的行为,RecursiveArrayIterator 提供了对嵌套数组的递归遍历支持,RecursiveIteratorIterator 则是驱动递归遍历的核心引擎。这套工具特别适合处理树形结构、嵌套数组和目录遍历。
核心组合
RecursiveIteratorIterator(RecursiveArrayIterator($nestedArray)) 是遍历嵌套数组的标准模式。
基础概念
RecursiveIterator 接口
RecursiveIterator 扩展了 Iterator 接口,新增了 hasChildren() 和 getChildren() 两个方法,用于判断和获取子迭代器。
RecursiveArrayIterator
RecursiveArrayIterator 是 ArrayIterator 的递归版本,自动将数组元素视为可递归的子结构。
RecursiveIteratorIterator
RecursiveIteratorIterator 接受一个 RecursiveIterator,自动处理递归遍历逻辑。
语法与代码
递归遍历嵌套数组
<?php
declare(strict_types=1);
$nested = [
'fruits' => [
'apple' => ['color' => 'red', 'price' => 3],
'banana' => ['color' => 'yellow', 'price' => 2],
],
'vegetables' => [
'carrot' => ['color' => 'orange', 'price' => 1],
],
];
$recursiveIterator = new \RecursiveArrayIterator($nested);
$iterator = new \RecursiveIteratorIterator($recursiveIterator);
foreach ($iterator as $key => $value) {
$depth = $iterator->getDepth();
$indent = str_repeat(' ', $depth);
echo "{$indent}{$key}: {$value}\n";
}
// color: red
// price: 3
// color: yellow
// price: 2
// color: orange
// price: 1获取完整路径键
<?php
declare(strict_types=1);
$data = [
'config' => [
'database' => [
'host' => 'localhost',
'port' => 3306,
],
'cache' => [
'driver' => 'redis',
'prefix' => 'app_',
],
],
];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($data)
);
foreach ($iterator as $value) {
$keys = [];
foreach (range(0, $iterator->getDepth()) as $depth) {
$keys[] = $iterator->getSubIterator($depth)->key();
}
$path = implode('.', $keys);
echo "{$path} = {$value}\n";
}
// config.database.host = localhost
// config.database.port = 3306
// config.cache.driver = redis
// config.cache.prefix = app_RecursiveIteratorIterator 遍历模式
<?php
declare(strict_types=1);
$tree = [
'root' => [
'child1' => ['leaf1', 'leaf2'],
'child2' => ['leaf3'],
],
];
$recursive = new \RecursiveArrayIterator($tree);
// 模式一:只遍历叶子节点(默认,LEAVES_ONLY)
$leavesOnly = new \RecursiveIteratorIterator($recursive);
echo "叶子节点:\n";
foreach ($leavesOnly as $key => $value) {
echo " {$value}\n";
}
// 模式二:遍历所有节点(SELF_FIRST)
$allNodes = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($tree),
\RecursiveIteratorIterator::SELF_FIRST
);
echo "所有节点:\n";
foreach ($allNodes as $key => $value) {
$depth = $allNodes->getDepth();
echo str_repeat(' ', $depth) . "{$key}: {$value}\n";
}遍历模式
LEAVES_ONLY:只遍历叶子节点(默认)SELF_FIRST:先父后子CHILD_FIRST:先子后父
目录递归遍历
<?php
declare(strict_types=1);
$directory = new \RecursiveDirectoryIterator(
'/path/to/project',
\FilesystemIterator::SKIP_DOTS
);
$iterator = new \RecursiveIteratorIterator(
$directory,
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $fileInfo) {
if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') {
$relative = $iterator->getSubIterator(0)->getSubPathname();
echo "{$relative}\n";
}
}详细说明
RecursiveIterator 接口方法
| 方法 | 说明 |
|---|---|
hasChildren() | 当前元素是否有子元素 |
getChildren() | 返回当前元素的子迭代器 |
RecursiveIteratorIterator 遍历模式常量
| 模式 | 值 | 说明 |
|---|---|---|
LEAVES_ONLY | 0 | 只返回叶子节点 |
SELF_FIRST | 1 | 先返回父节点再返回子节点 |
CHILD_FIRST | 2 | 先返回子节点再返回父节点 |
getDepth() 和 getSubIterator()
getDepth()— 当前递归深度(从 0 开始)getSubIterator($depth)— 获取指定深度的子迭代器
实战示例
实战:扁平化嵌套数组
<?php
declare(strict_types=1);
function flattenArray(array $array): array
{
$result = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($array)
);
foreach ($iterator as $value) {
$result[] = $value;
}
return $result;
}
$nested = [1, [2, [3, [4, [5]]]]];
$flat = flattenArray($nested);
print_r($flat);
// Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 4 [4] => 5 )实战:递归搜索嵌套数据
<?php
declare(strict_types=1);
function searchNested(array $data, string $search): \Generator
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($data),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $key => $value) {
if (is_string($value) && str_contains($value, $search)) {
$pathKeys = [];
for ($i = 0; $i <= $iterator->getDepth(); $i++) {
$pathKeys[] = $iterator->getSubIterator($i)->key();
}
yield implode('.', $pathKeys) => $value;
}
}
}
$config = [
'app' => ['name' => 'MyApp', 'version' => '1.0'],
'db' => ['host' => 'localhost', 'name' => 'mydb'],
];
foreach (searchNested($config, 'my') as $path => $value) {
echo "找到: {$path} = {$value}\n";
}注意事项
RecursiveArrayIterator 的类型安全
RecursiveArrayIterator 的 hasChildren() 在子元素是数组时返回 true。对于非数组元素,不会尝试递归。
性能考虑
递归遍历深层嵌套结构时,RecursiveIteratorIterator 的性能通常优于手动递归。
最佳实践
- 选择合适的遍历模式:根据需求选择
LEAVES_ONLY、SELF_FIRST或CHILD_FIRST。 - 使用 getDepth() 格式化输出:递归深度信息对于格式化嵌套输出非常有用。
- 结合 FilterIterator:可以在递归迭代器外层包装过滤逻辑。
<?php
declare(strict_types=1);
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator('.', \FilesystemIterator::SKIP_DOTS)
);
$phpFiles = new \CallbackFilterIterator(
$files,
fn(\SplFileInfo $file): bool => $file->isFile() && $file->getExtension() === 'php'
);RecursiveIteratorIterator 详解
获取遍历路径
<?php
declare(strict_types=1);
$menu = [
'首页' => [
'产品中心' => [
'手机' => ['iPhone', 'Android'],
'电脑' => ['笔记本', '台式机'],
],
'关于我们' => [
'公司简介',
'联系方式',
],
],
];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($menu),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $value) {
$depth = $iterator->getDepth();
$prefix = str_repeat(' ', $depth);
$path = [];
for ($i = 0; $i <= $depth; $i++) {
$path[] = $iterator->getSubIterator($i)->key();
}
$pathStr = implode(' > ', $path);
echo "{$prefix}{$value} [{$pathStr}]\n";
}
// 首页 [首页]
// 产品中心 [首页 > 产品中心]
// 手机 [首页 > 产品中心 > 手机]
// iPhone [首页 > 产品中心 > 手机 > 0]
// Android [首页 > 产品中心 > 手机 > 1]递归迭代器的迭代计数
<?php
declare(strict_types=1);
function countNestedElements(array $data): int
{
$count = 0;
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($data)
);
foreach ($iterator as $_) {
$count++;
}
return $count;
}
$tree = [
'level1' => [
'level2a' => [1, 2, 3],
'level2b' => [4, 5],
],
'another' => ['x', 'y', 'z'],
];
echo "总叶子节点: " . countNestedElements($tree) . "\n"; // 8递归过滤嵌套数据
<?php
declare(strict_types=1);
function filterNested(array $data, callable $predicate): array
{
$result = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveArrayIterator($data),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $key => $value) {
if ($predicate($value, $key, $iterator->getDepth())) {
$path = [];
for ($i = 0; $i <= $iterator->getDepth(); $i++) {
$path[] = $iterator->getSubIterator($i)->key();
}
$current = &$result;
foreach ($path as $segment) {
if (!isset($current[$segment])) {
$current[$segment] = [];
}
$current = &$current[$segment];
}
$current = $value;
unset($current);
}
}
return $result;
}自定义 RecursiveIterator
<?php
declare(strict_types=1);
class TreeNodeIterator implements \RecursiveIterator
{
private int $position = 0;
/** @var array<int, TreeNode> */
private array $children;
public function __construct(private readonly TreeNode $node)
{
$this->children = $node->getChildren();
}
public function current(): TreeNode
{
return $this->children[$this->position];
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->position++;
}
public function rewind(): void
{
$this->position = 0;
}
public function valid(): bool
{
return isset($this->children[$this->position]);
}
public function hasChildren(): bool
{
return $this->current()->hasChildren();
}
public function getChildren(): \RecursiveIterator
{
return new self($this->current());
}
}实战:目录结构分析器
<?php
declare(strict_types=1);
function analyzeDirectory(string $path): array
{
$stats = [
'totalFiles' => 0,
'totalDirs' => 0,
'extensions' => [],
'largestFile' => ['name' => '', 'size' => 0],
];
$directory = new \RecursiveDirectoryIterator(
$path,
\FilesystemIterator::SKIP_DOTS
);
$iterator = new \RecursiveIteratorIterator(
$directory,
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$stats['totalFiles']++;
$ext = $file->getExtension();
$stats['extensions'][$ext] = ($stats['extensions'][$ext] ?? 0) + 1;
if ($file->getSize() > $stats['largestFile']['size']) {
$stats['largestFile'] = [
'name' => $file->getFilename(),
'size' => $file->getSize(),
];
}
}
}
return $stats;
}常见误区与 FAQ
RecursiveArrayIterator 如何处理对象元素?
RecursiveArrayIterator 的 hasChildren() 只在子元素是数组或对象时返回 true。如果嵌套数据中混合了对象和标量值,需要自定义 RecursiveIterator 实现。
SELF_FIRST vs CHILD_FIRST 什么时候用哪个?
SELF_FIRST(先父后子):适合构建树形菜单、目录结构展示CHILD_FIRST(先子后父):适合删除操作(先删除子节点再删除父节点)、自底向上的计算
递归深度过大会有问题吗?
理论上没有限制,但 PHP 有调用栈深度限制。对于极深嵌套的结构(如数万层级),可能需要调整 xdebug.max_stack_frames 配置。