Skip to content

RecursiveIterator / RecursiveArrayIterator / RecursiveIteratorIterator

概述

PHP SPL 提供了一组递归迭代器,用于遍历嵌套数据结构。RecursiveIterator 接口定义了递归迭代的行为,RecursiveArrayIterator 提供了对嵌套数组的递归遍历支持,RecursiveIteratorIterator 则是驱动递归遍历的核心引擎。这套工具特别适合处理树形结构、嵌套数组和目录遍历。

核心组合

RecursiveIteratorIterator(RecursiveArrayIterator($nestedArray)) 是遍历嵌套数组的标准模式。

基础概念

RecursiveIterator 接口

RecursiveIterator 扩展了 Iterator 接口,新增了 hasChildren()getChildren() 两个方法,用于判断和获取子迭代器。

RecursiveArrayIterator

RecursiveArrayIteratorArrayIterator 的递归版本,自动将数组元素视为可递归的子结构。

RecursiveIteratorIterator

RecursiveIteratorIterator 接受一个 RecursiveIterator,自动处理递归遍历逻辑。

语法与代码

递归遍历嵌套数组

php
<?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
<?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
<?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
<?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_ONLY0只返回叶子节点
SELF_FIRST1先返回父节点再返回子节点
CHILD_FIRST2先返回子节点再返回父节点

getDepth() 和 getSubIterator()

  • getDepth() — 当前递归深度(从 0 开始)
  • getSubIterator($depth) — 获取指定深度的子迭代器

实战示例

实战:扁平化嵌套数组

php
<?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
<?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 的类型安全

RecursiveArrayIteratorhasChildren() 在子元素是数组时返回 true。对于非数组元素,不会尝试递归。

性能考虑

递归遍历深层嵌套结构时,RecursiveIteratorIterator 的性能通常优于手动递归。

最佳实践

  1. 选择合适的遍历模式:根据需求选择 LEAVES_ONLYSELF_FIRSTCHILD_FIRST
  2. 使用 getDepth() 格式化输出:递归深度信息对于格式化嵌套输出非常有用。
  3. 结合 FilterIterator:可以在递归迭代器外层包装过滤逻辑。
php
<?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
<?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
<?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
<?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
<?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
<?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 如何处理对象元素?

RecursiveArrayIteratorhasChildren() 只在子元素是数组或对象时返回 true。如果嵌套数据中混合了对象和标量值,需要自定义 RecursiveIterator 实现。

SELF_FIRST vs CHILD_FIRST 什么时候用哪个?

  • SELF_FIRST(先父后子):适合构建树形菜单、目录结构展示
  • CHILD_FIRST(先子后父):适合删除操作(先删除子节点再删除父节点)、自底向上的计算

递归深度过大会有问题吗?

理论上没有限制,但 PHP 有调用栈深度限制。对于极深嵌套的结构(如数万层级),可能需要调整 xdebug.max_stack_frames 配置。

参考链接