标题:PHP 递归遍历未知深度的多维 API 数组(避免嵌套 foreach)

本文介绍一种简洁、可扩展的递归方法,替代传统多层 foreach 循环,高效提取 json api 返回的嵌套数组中同时包含 `value` 和 `id` 的 header 字段及其紧邻的 rows 数据。

当从财务类 API(如 QuickBooks Online 报表接口)获取结构灵活但语义一致的嵌套 JSON 数据时,开发者常面临“深度不可预知”的挑战:Rows → Row → Rows → Row → ... 可能嵌套 3 层、7 层甚至更深,而关键信息(如账户名称 "40000 Sales Income" 及其 ID "31")总出现在某一层的 Header.ColData 中,其对应明细数据则紧随其后位于同级或下级的 Rows.Row.*.ColData 中。

硬编码多层 foreach 不仅易错、难维护,更无法应对动态报告结构(如“损益表”与“应收账款明细”深度差异大)。真正的解法是递归 + 精准路径守卫(Path Guarding)——即在每一层递归中,只关注当前节点是否满足业务提取条件,而非预设层级。

以下是一个健壮、可复用的递归函数实现:

function extractHeaderWithIdAndChildren(array $data): array {
    $results = [];

    // 安全遍历 Rows.Row(兼容缺失、空或非数组情况)
    $rows = $data['Rows']['Row'] ?? [];
    if (!is_array($rows)) {
        return $results;
    }

    foreach ($rows as $row) {
        // ✅ 条件一:存在 Header.ColData 且至少一个子项含 value + id
        $headerColData = $row['Header']['ColData'] ?? [];
        $hasValidHeader = false;
        $validHeaderItem = null;

        foreach ($headerColData as $col) {
            if (isset($col['value'], $col['id']) && is_string($col['value']) && is_string($col['id'])) {
                $hasValidHeader = true;
                $validHeaderItem = $col;
                break; // 取首个有效项(通常 Header 中仅首列含 id)
            }
        }

        if ($hasValidHeader && $validHeaderItem) {
            // 提取核心标识信息
            $extracted = [
                'value' => $validHeaderItem['value'],
                'id'    => $validHeaderItem['id']
            ];

            // ✅ 条件二:提取“紧邻的后续 ColData”——优先取同级 Rows.Row 下的 ColData(即子行明细)
            // 注意:根据示例结构,目标数据实际在 $row['Rows']['Row'][0]['ColData'](type: "Data")
            $immediateColData = [];
            $nestedRows = $row['Rows']['Row'] ?? [];
            if (is_array($nestedRows) && !empty($nestedRows)) {
                // 遍历子 Row,收集所有 type === "Data" 的 ColData(避免 Summary/Section)
                foreach ($nestedRows as $childRow) {
                    if (isset($childRow['type']) && $childRow['type'] === 'Data' && isset($childRow['ColData'])) {
                        $immediateColData[] = $childRow['ColData'];
                    }
                }
            }

            $results[] = [
                'header' => $extracted,
                'details' => $immediateColData // 多条明细,每条为 ColData 数组
            ];
        } else {
            // ❌ 当前行不匹配 → 递归进入其子结构(如深层嵌套的 Rows.Row)
            // 注意:跳过 Summary/Section 等非数据节点,聚焦 Rows 分支
            if (isset($row['Rows']) && is_array($row['Rows'])) {
                $results = array_merge($results, extractHeaderWithIdAndChildren($row));
            }
        }
    }

    return $results;
}

// 使用示例:
$json = file_get_contents('api_response.json'); // 或来自 cURL 响应
$data = json_decode($json, true);

$extracted = extractHeaderWithIdAndChildren($data);
print_r($extracted);

关键设计说明:

  • 防御性访问:全程使用 ?? [] 和 isset() 避免 Undefined index,适配 API 字段缺失场景;
  • 语义化判断:不仅检查 value/id 存在,还验证类型(is_string),防止空值或数字 ID 导致逻辑错误;
  • 精准定位明细:不再盲目取 $row['Rows']['Row'][0]['ColData'],而是筛选 type === "Data" 的节点,自动忽略 Summary(报表汇总)和 Section(分组标题);
  • 扁平化结果:无论原始嵌套多深,最终输出统一结构的 ['header' => [...], 'details' => [...]],便于后续导出 CSV 或存入数据库。

⚠️ 注意事项:

  • 若 API 返回极深嵌套(>100 层),需考虑 PHP 默认 xdebug.max_nesting_level 限制,可临时调高或改用栈式迭代(Stack-based DFS)避免递归栈溢出;
  • 对超大响应(如原文提到的 25KB+),建议配合 json_decode($json, true, 512, JSON_BIGINT_AS_STRING) 防止大整数 ID 被转为科学计数法;
  • 生产环境务必添加日志记录与异常捕获,例如对 json_decode 失败做 json_last_error() 检查。

通过将“结构不确定性”转化为“逻辑确定性”,该方案让代码专注业务意图(找带 ID 的 Header,取它的数据子项),而非纠结于括号嵌套层数——这才是处理动态 API 数据的现代 PHP 实践。