You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP递归替换数组占位符时新增元素触发死循环问题排查

问题:数组占位符替换时的无限循环问题

原始需求

给定原始数组:

$data = [
    ["id" => 1, "data" => "data 1"],
    ["id" => 2, "data" => "data <4>"],
    ["id" => 3, "data" => "data 3"],
    ["id" => 4, "data" => "<3>"]
];

需要生成新数组,将data字段中<数字>格式的占位符替换为对应id元素的data值,预期结果:

[
    ["id" => 1, "data" => "data 1"],
    ["id" => 2, "data" => "data data 3"],
    ["id" => 3, "data" => "data 3"],
    ["id" => 4, "data" => "data 3"]
]

现有实现代码

以下是实现该逻辑的函数:

public function refProcess($data, &$newData, $i, &$tmpData){
    $dataLength = count($data);
    if($i>=$dataLength){
        return;
    }
    for(;$i<$dataLength;$i++){
        if(is_null($tmpData)){
            $tmpData = ['id'=> $data[$i]['id'], 'data'=>null];
        }

        if(strpos($data[$i]['data'],'[')!==false){

            $parsed = $this->getInbetweenStrings("<", ">", $data[$i]['data']);
            if(count($parsed)){

                foreach($parsed as $occurance){
                    foreach($data as $key => $dataValue){
                        if($dataValue['id']==$occurance){

                            if(strpos($dataValue['data'], "<")!==false){

                                $this->refProcess($data, $newData, $key, $tmpData);
                                $tmpData=null;
                            }
                            else{
                                
                                $tmpDataAtt = str_replace("<".$occurance.">", $dataValue['data'], $data[$i]['data']);
                                $tmpData['data'] = $tmpDataAtt;
                                $newData [] = $tmpData;
                                $tmpData = null;
                                break;
                            }
                        }
                    }
                    
                }
            }
        }
        else{
            $tmpData['data'] = $data[$i]['data'];
            $newData [] = $tmpData;
            $tmpData = null;
        }
    }//foreach
}

// 返回$start和$end之间的字符串数组,支持多匹配
public function getInbetweenStrings($start, $end, $str){
    $matches = array();
    $regex = "/$start([a-zA-Z0-9_]*)$end/";
    preg_match_all($regex, $str, $matches);
    return $matches[1];
}

问题复现

当新增元素后:

$data = [
    ["id" => 1, "data" => "data 1"],
    ["id" => 2, "data" => "data <4>"],
    ["id" => 3, "data" => "data 3"],
    ["id" => 4, "data" => "<3>"],
    ["id" => 5, "data" => "<2>"]
];

处理id=5的元素时,函数陷入无限循环。


问题根源

  1. 递归与遍历逻辑耦合:refProcess函数同时负责遍历数组和递归解析占位符,递归调用时会重复触发对同一元素的处理(比如处理id=4时,递归处理id=3后,循环会再次处理id=4,进而再次递归id=3,形成无限循环)。
  2. 未检测引用链:当出现id5→id2→id4→id3的引用链时,函数没有记录已处理的元素路径,导致重复进入递归流程。
  3. 占位符判断错误:代码中用strpos($data[$i]['data'],'[')判断占位符,实际应检测<,属于低级bug。

修复方案

拆分遍历与递归逻辑,引入缓存和循环引用检测:

class Processor {
    private $parsedCache = [];

    public function processData($data) {
        $newData = [];
        // 建立id到元素的映射,快速查找
        $idMap = array_column($data, null, 'id');
        
        foreach ($data as $item) {
            $newData[] = [
                'id' => $item['id'],
                'data' => $this->parseData($item['data'], $idMap)
            ];
        }
        
        return $newData;
    }

    private function parseData($dataStr, $idMap, $visited = []) {
        // 提取所有占位符
        preg_match_all('/<(\d+)>/', $dataStr, $matches);
        
        if (empty($matches[1])) {
            return $dataStr;
        }
        
        foreach ($matches[1] as $id) {
            $id = (int)$id;
            // 检测循环引用
            if (in_array($id, $visited)) {
                throw new Exception("循环引用:id {$id} 在引用链中重复出现");
            }
            // 缓存已解析结果,避免重复处理
            if (!isset($this->parsedCache[$id])) {
                if (!isset($idMap[$id])) {
                    throw new Exception("未找到id为 {$id} 的元素");
                }
                // 递归解析,传递已访问id列表
                $this->parsedCache[$id] = $this->parseData($idMap[$id]['data'], $idMap, array_merge($visited, [$id]));
            }
            // 替换占位符
            $dataStr = str_replace("<{$id}>", $this->parsedCache[$id], $dataStr);
        }
        
        return $dataStr;
    }
}

// 使用示例
$data = [
    ["id" => 1, "data" => "data 1"],
    ["id" => 2, "data" => "data <4>"],
    ["id" => 3, "data" => "data 3"],
    ["id" => 4, "data" => "<3>"],
    ["id" => 5, "data" => "<2>"]
];

$processor = new Processor();
$result = $processor->processData($data);
print_r($result);

该方案的优势:

  • 用id映射替代遍历查找,提升效率
  • 缓存已解析结果,避免重复计算
  • 检测循环引用,从根源防止无限递归
  • 递归仅负责字符串解析,逻辑清晰,避免遍历与递归的耦合问题

内容的提问来源于stack exchange,提问作者xoxaro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 00:50:45