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的元素时,函数陷入无限循环。
问题根源
- 递归与遍历逻辑耦合:
refProcess函数同时负责遍历数组和递归解析占位符,递归调用时会重复触发对同一元素的处理(比如处理id=4时,递归处理id=3后,循环会再次处理id=4,进而再次递归id=3,形成无限循环)。 - 未检测引用链:当出现
id5→id2→id4→id3的引用链时,函数没有记录已处理的元素路径,导致重复进入递归流程。 - 占位符判断错误:代码中用
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
相关产品推荐
相关产品推荐

