> 技术文档 > XML高效处理类 - 专为Office文档XML处理优化

XML高效处理类 - 专为Office文档XML处理优化

/**
 *
 * 提供XML读取、写入、修改、查询等高级功能,支持命名空间和复杂XML结构
 * 
 * 主要功能:
 * 1. 复杂路径解析(支持属性筛选、索引、通配符)
 * 2. 完整节点类型支持(元素、文本、CDATA、注释、PI)
 * 3. 高效元素/属性操作(增删改查、复制、移动)
 * 4. 流式处理(低内存占用,适合大型XML)
 */

reader = new XMLReader(); $this->writer = new XMLWriter(); $this->writer->setIndent(true); $this->writer->setIndentString(\' \'); } /** * 注册命名空间(支持双向映射,避免前缀冲突) * @param string $prefix 命名空间前缀 * @param string $uri 命名空间URI */ public function addNamespace(string $prefix, string $uri): void { $this->namespaces[$prefix] = $uri; $this->namespaceUris[$uri] = $prefix; } /** * 解析XML为数组(流式解析,低内存占用) * @param string $xmlContent XML内容 * @param bool $preserveAttributes 是否保留属性(键名带@前缀) * @return array 解析后的数据数组 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function parseToArray(string $xmlContent, bool $preserveAttributes = true): array { $result = []; $stack = []; $current = &$result; $this->processXmlContent($xmlContent, function () use (&$current, &$stack, $preserveAttributes) { $nodeType = $this->reader->nodeType; $nodeName = $this->reader->name; // 处理开始元素 if ($nodeType === self::NODE_ELEMENT) { $element = []; // 处理属性 if ($preserveAttributes && $this->reader->hasAttributes) {  $attrs = [];  while ($this->reader->moveToNextAttribute()) { $attrs[\'@\' . $this->reader->name] = $this->reader->value;  }  $this->reader->moveToElement();  $element = array_merge($element, $attrs); } // 处理子节点容器 $element[\'#children\'] = []; $childKey = $nodeName; // 处理重复节点(转为数组) if (isset($current[$childKey])) {  if (!is_array($current[$childKey]) || !isset($current[$childKey][0])) { $current[$childKey] = [$current[$childKey]];  }  $childIndex = count($current[$childKey]);  $current[$childKey][$childIndex] = &$element;  $stack[] = &$current;  $stack[] = $childKey;  $stack[] = $childIndex;  $current = &$current[$childKey][$childIndex][\'#children\']; } else {  $current[$childKey] = &$element;  $stack[] = &$current;  $stack[] = $childKey;  $stack[] = null;  $current = &$current[$childKey][\'#children\']; } // 空元素处理 if ($this->reader->isEmptyElement) {  unset($element[\'#children\']); // 空元素无children  array_pop($stack); // 移除childIndex  array_pop($stack); // 移除childKey  $parent = &$stack[array_pop($stack)];  $current = &$parent; } } // 处理结束元素 elseif ($nodeType === self::NODE_END_ELEMENT) { if (empty($current)) {  array_pop($stack); // 移除childIndex  $childKey = array_pop($stack);  $parent = &$stack[array_pop($stack)];  unset($parent[$childKey][\'#children\']); // 无children则移除键 } else {  array_pop($stack); // 移除childIndex  $childKey = array_pop($stack);  $parent = &$stack[array_pop($stack)]; } $current = &$parent; } // 处理文本/CDATA节点 elseif (in_array($nodeType, [self::NODE_TEXT, self::NODE_CDATA])) { $value = $this->reader->value; if (empty($current)) {  $current[\'#text\'] = $value; } else {  $current[] = [\'#text\' => $value]; } } // 处理注释节点 elseif ($nodeType === self::NODE_COMMENT) { $current[\'#comment\'] = $this->reader->value; } // 处理PI节点 elseif ($nodeType === self::NODE_PI) { $current[\'#pi_\' . $nodeName] = $this->reader->value; } return true; }); return $result; } /** * 创建新的XML文档(增强版) * @param string $rootElement 根元素名称,支持命名空间前缀(格式:prefix:element) * @param array $attributes 根元素属性 * @param string $version XML版本 * @param string $encoding 编码格式 * @return string 创建的XML内容 */ public function createDocument( string $rootElement, array $attributes = [], string $version = \'1.0\', string $encoding = \'UTF-8\' ): string { $this->writer->openMemory(); $this->writer->startDocument($version, $encoding); // 处理根元素(带命名空间) $this->startElement($rootElement); $this->writeAttributes($attributes); $this->writer->endElement(); // 关闭根元素 $this->writer->endDocument(); return $this->writer->outputMemory(); } /** * 读取XML文件(支持编码检测) * @param string $filePath XML文件路径 * @param string $encoding 预期编码(默认UTF-8) * @return string XML内容 * @throws DocxProcessingException 当文件无法打开时抛出 */ public function readFile(string $filePath, string $encoding = \'UTF-8\'): string {  $context = stream_context_create([\'http\' => [\'encoding\' => $encoding]]); if (!$this->reader->open($filePath, $encoding, LIBXML_NONET, $context)) { throw new DocxProcessingException(\'Failed to open XML file: \' . $filePath); } $this->writer->openMemory(); $this->processXml(); $this->reader->close(); return $this->writer->outputMemory(); } /** * 向XML添加子元素(支持复杂路径和插入位置) * @param string $xmlString XML内容 * @param string $parentPath 父元素路径(支持属性筛选:parent/child[@attr=\"val\"]) * @param string $childName 子元素名称,支持命名空间前缀 * @param string $childValue 子元素文本值(支持CDATA:前缀加\'cdata:\'则自动包裹) * @param array $attributes 子元素属性数组 * @param bool $prepend 是否前置插入(默认后置) * @return string 更新后的XML内容 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function addElement( string $xmlString, string $parentPath, string $childName, string $childValue = \'\', array $attributes = [], bool $prepend = false ): string { $pathParser = $this->createPathParser($parentPath); return $this->modifyXml($xmlString, function ($writer, $currentPath, $depth) use ( $pathParser, $childName, $childValue, $attributes, $prepend ) { static $added = false; $currentNodePath = implode(\'/\', $currentPath); // 前置插入:在父元素开始标签后立即插入 if ($this->isElementNode() && !$added) { if ($pathParser->matches($currentNodePath, $this->reader)) {  $this->writeElement($childName, $childValue, $attributes);  $added = true; } } // 后置插入:在父元素结束标签前插入 if ($this->isEndElementNode() && !$added) { $parentPath = implode(\'/\', $currentPath); if ($pathParser->matches($parentPath, $this->reader)) {  $this->writeElement($childName, $childValue, $attributes);  $added = true; } } return false; }); } /** * 更新XML元素值(支持复杂路径和多节点) * @param string $xmlString XML内容 * @param string $elementPath 元素路径(支持通配符和属性筛选) * @param string $newValue 新的元素值(支持CDATA:前缀加\'cdata:\') * @param int $maxUpdates 最大更新数量(-1表示全部) * @return string 更新后的XML内容 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function updateValue( string $xmlString, string $elementPath, string $newValue, int $maxUpdates = -1 ): string { $pathParser = $this->createPathParser($elementPath); return $this->modifyXml($xmlString, function ($writer, $currentPath, $depth) use ( $pathParser, $newValue, $maxUpdates ) { static $updatedCount = 0; $currentNodePath = implode(\'/\', $currentPath); // 检查是否达到最大更新数量 if ($maxUpdates > 0 && $updatedCount >= $maxUpdates) { return false; } // 匹配目标元素且为文本节点 if ($this->isTextNode() && $pathParser->matches($currentNodePath, $this->reader)) { // 处理CDATA if (strpos($newValue, \'cdata:\') === 0) {  $this->writer->writeCData(substr($newValue, 5)); } else {  $this->writer->text($newValue); } $updatedCount++; return true; // 跳过原文本 } return false; }); } /** * 删除XML元素(支持复杂路径和批量删除) * @param string $xmlString XML内容 * @param string $elementPath 元素路径(支持通配符和属性筛选) * @param int $maxDeletions 最大删除数量(-1表示全部) * @return string 更新后的XML内容 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function removeElement(string $xmlString, string $elementPath, int $maxDeletions = -1): string { $pathParser = $this->createPathParser($elementPath); return $this->modifyXml($xmlString, function ($writer, $currentPath, $depth) use ( $pathParser, $maxDeletions ) { static $skip = false, $targetDepth = 0, $deletionCount = 0; // 跳过被删除元素的子节点 if ($skip) { if ($this->isEndElementNode() && $depth  0 && $deletionCount >= $maxDeletions) { return false; } // 匹配目标元素则标记跳过 if ($this->isElementNode()) { $currentNodePath = implode(\'/\', $currentPath); if ($pathParser->matches($currentNodePath, $this->reader)) {  $skip = true;  $targetDepth = $depth - 1;  return true; // 跳过元素本身 } } return false; }); } /** * 复制元素到指定位置 * @param string $xmlString XML内容 * @param string $sourcePath 源元素路径(支持复杂路径) * @param string $targetParentPath 目标父元素路径 * @param string|null $newName 新元素名称(null则保留原名) * @param bool $keepSource 是否保留源元素(默认保留) * @return string 更新后的XML内容 * @throws DocxProcessingException 当元素不存在时抛出 */ public function copyElement( string $xmlString, string $sourcePath, string $targetParentPath, ?string $newName = null, bool $keepSource = true ): string { // 提取源元素XML片段 $sourceXml = $this->getOuterXml($xmlString, $sourcePath); if ($sourceXml === null) { throw new DocxProcessingException(\"Source element not found: {$sourcePath}\"); } // 替换元素名称(如需要) if ($newName) { $sourceXml = preg_replace(\'/^]+>/\', \"\", $sourceXml, 1); $sourceXml = preg_replace(\'/]+>$/\', \"\", $sourceXml, 1); } // 插入到目标位置 $result = $this->addElement( $xmlString, $targetParentPath, \'\', // 临时名称(实际用XML片段) $sourceXml, [], false ); // 不保留源元素则删除 return $keepSource ? $result : $this->removeElement($result, $sourcePath, 1); } /** * 移动元素到新位置(本质是复制+删除源) * @param string $xmlString XML内容 * @param string $sourcePath 源元素路径 * @param string $targetParentPath 目标父元素路径 * @return string 更新后的XML内容 */ public function moveElement(string $xmlString, string $sourcePath, string $targetParentPath): string { return $this->copyElement($xmlString, $sourcePath, $targetParentPath, null, false); }/** * 获取元素的完整XML片段(outer XML) * @param string $xmlString XML内容 * @param string $elementPath 元素路径 * @return string|null 元素的完整XML片段,未找到则返回null * @throws DocxProcessingException 当XML解析失败时抛出 */ public function getOuterXml(string $xmlString, string $elementPath): ?string { $pathParser = $this->createPathParser($elementPath); $fragment = null; $captureWriter = new XMLWriter(); $captureWriter->openMemory(); $this->processXmlContent($xmlString, function () use ( $pathParser, $captureWriter, &$fragment ) { static $capturing = false, $targetDepth = 0; if ($capturing) { // 捕获元素的所有节点(包括子节点) $this->copyNodeToWriter($this->reader, $captureWriter); // 捕获结束:当遇到目标深度的结束标签 if ($this->isEndElementNode() && $this->reader->depth === $targetDepth) {  $capturing = false;  $fragment = $captureWriter->outputMemory();  return false; // 停止解析 } return true; } // 开始捕获:匹配目标元素 if ($this->isElementNode()) { $currentPath = $this->buildCurrentPath(); if ($pathParser->matches($currentPath, $this->reader)) {  $capturing = true;  $targetDepth = $this->reader->depth;  $this->copyNodeToWriter($this->reader, $captureWriter); // 捕获开始标签 } } return true; }); return $fragment; }/** * 检查元素是否存在(高效方法) * @param string $xmlString XML内容 * @param string $elementPath 元素路径 * @return bool 是否存在 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function exists(string $xmlString, string $elementPath): bool { $pathParser = $this->createPathParser($elementPath); $exists = false; $this->processXmlContent($xmlString, function () use ($pathParser, &$exists) { if ($this->isElementNode()) { $currentPath = $this->buildCurrentPath(); if ($pathParser->matches($currentPath, $this->reader)) {  $exists = true;  return false; // 找到则停止 } } return true; }); return $exists; } /** * 查找所有匹配路径的元素(增强版) * @param string $xmlString XML内容 * @param string $elementPath 元素路径(支持通配符、属性筛选、索引) * @return array 匹配元素数组,每个元素包含: *  - value: 文本值 *  - attributes: 属性数组 *  - outer_xml: 完整XML片段 *  - path: 元素路径 *  - depth: 深度 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function query(string $xmlString, string $elementPath): array { $pathParser = $this->createPathParser($elementPath); $results = []; $currentElement = null; $currentWriter = new XMLWriter(); $this->processXmlContent($xmlString, function () use ( $pathParser, &$results, &$currentElement, $currentWriter ) { if ($this->isElementNode()) { $currentPath = $this->buildCurrentPath(); if ($pathParser->matches($currentPath, $this->reader)) {  // 初始化当前元素信息  $currentElement = [ \'value\' => \'\', \'attributes\' => $this->getAllAttributes(), \'path\' => $currentPath, \'depth\' => $this->reader->depth, \'outer_xml\' => \'\'  ];  $results[] = &$currentElement;  $currentWriter->openMemory();  $this->copyNodeToWriter($this->reader, $currentWriter); // 记录开始标签 } } // 收集元素内文本 if ($currentElement && $this->isTextNode() && $this->reader->depth === $currentElement[\'depth\'] + 1) { $currentElement[\'value\'] .= $this->reader->value; } // 记录outer_xml(直到元素结束) if ($currentElement && $this->reader->depth >= $currentElement[\'depth\']) { if (!$this->isElementNode() || $this->reader->depth !== $currentElement[\'depth\']) {  $this->copyNodeToWriter($this->reader, $currentWriter); } // 元素结束时保存outer_xml if ($this->isEndElementNode() && $this->reader->depth === $currentElement[\'depth\']) {  $currentElement[\'outer_xml\'] = $currentWriter->outputMemory();  $currentElement = null; } } return true; }); return $results; } /** * (增强版)更新XML元素的属性 * @param string $xmlString XML内容 * @param string $elementPath 元素路径(支持复杂路径) * @param string $attributeName 属性名称 * @param string $newValue 新的属性值 * @param bool $addIfMissing 当属性不存在时是否添加 * @return string 更新后的XML内容 * @throws DocxProcessingException 当XML解析失败时抛出 */ public function updateAttribute( string $xmlString, string $elementPath, string $attributeName, string $newValue, bool $addIfMissing = true ): string { $pathParser = $this->createPathParser($elementPath); return $this->modifyXml($xmlString, function ($writer, $currentPath, $depth) use ( $pathParser, $attributeName, $newValue, $addIfMissing ) { static $updatedCount = 0; if ($this->isElementNode()) { $currentNodePath = implode(\'/\', $currentPath); if ($pathParser->matches($currentNodePath, $this->reader)) {  // 写入开始标签  $this->startElementWithNamespace();  // 处理属性(更新或添加)  $attrs = $this->getAllAttributes();  $attrExists = isset($attrs[$attributeName]);  if ($attrExists || $addIfMissing) { $attrs[$attributeName] = $newValue;  }  $this->writeAttributes($attrs);  // 空元素处理  if ($this->reader->isEmptyElement) { $this->writer->endElement();  }  $updatedCount++;  return true; // 跳过默认处理 } } return false; }); } /** * 替换整个元素(包括子元素) * @param string $xmlString XML内容 * @param string $elementPath 元素路径 * @param string $newValue 新文本值 * @param array $newAttributes 新属性数组 * @return string 更新后的XML */ public function replaceElement( string $xmlString, string $elementPath, string $newValue = \'\', array $newAttributes = [] ): string { $pathParser = $this->createPathParser($elementPath); return $this->modifyXml($xmlString, function ($writer, $currentPath, $depth) use ( $pathParser, $newValue, $newAttributes ) { static $replacing = false, $targetDepth = 0; $currentNodePath = implode(\'/\', $currentPath); // 处理元素开始标签 if ($this->isElementNode() && !$replacing) { if ($pathParser->matches($currentNodePath, $this->reader)) {  $replacing = true;  $targetDepth = $depth;  // 写入新元素开始标签  $this->startElementWithNamespace();  $this->writeAttributes($newAttributes);  // 处理值替换  if ($newValue !== \'\') { $this->writer->text($newValue); $this->writer->endElement(); return true;  }  return true; // 只更新属性,保留内容 } } // 处理元素结束标签 if ($this->isEndElementNode() && $replacing && $depth === $targetDepth) { $replacing = false; if ($newValue === \'\') {  $this->writer->endElement(); } return true; } // 跳过被替换元素的内容 if ($replacing) { return true; } return false; }); } /** * 批量更新匹配元素 * @param string $xmlString XML内容 * @param string $elementPath 元素路径 * @param callable $updater 更新回调 function(string $value, array $attrs): array * @return string 更新后的XML */ public function batchUpdateElements( string $xmlString, string $elementPath, callable $updater ): string { $pathParser = $this->createPathParser($elementPath, true); return $this->modifyXml($xmlString, function ($writer, $currentPath, $depth) use ($pathParser, $updater) { static $updating = false, $targetDepth = 0, $currentValue = \'\', $currentAttrs = []; $currentNodePath = implode(\'/\', $currentPath); // 开始元素处理 if ($this->isElementNode() && $pathParser->matches($currentNodePath, $this->reader)) { $updating = true; $targetDepth = $depth; $currentValue = \'\'; $currentAttrs = $this->getAllAttributes(); // 立即更新属性 [$newValue, $newAttrs] = $updater(\'\', $currentAttrs); $this->startElementWithNamespace(); $this->writeAttributes($newAttrs); return true; } // 收集文本内容 if ($updating && $this->isTextNode() && $depth === $targetDepth + 1) { $currentValue .= $this->reader->value; return true; } // 结束元素处理 if ($this->isEndElementNode() && $updating && $depth === $targetDepth) { $updating = false; // 应用最终更新 [$finalValue, $finalAttrs] = $updater($currentValue, $currentAttrs); $this->writer->text($finalValue); $this->writer->endElement(); return true; } return false; }); } // 以下为内部辅助方法(保持原实现) /** * 路径解析器(支持复杂路径语法) * @param string $path 路径字符串(如:parent/child[@id=\"1\"][2]、root/* * @return object 包含matches方法的解析器对象 */ private function createPathParser(string $path): object { $segments = explode(\'/\', $path); $filters = []; $index = null; // 解析每段路径中的筛选条件和索引 foreach ($segments as &$segment) { // 解析索引:如element[2] if (preg_match(\'/(.*)\\[(\\d+)\\]$/\', $segment, $m)) { $segment = $m[1]; $index = (int)$m[2] - 1; // 转为0基索引 } // 解析属性筛选:如element[@attr=\"val\"] if (preg_match(\'/(.*)\\[@([^=]+)=[\"\\\']([^\"\\\']+)[\"\\\']\\]/\', $segment, $m)) { $segment = $m[1]; $filters[] = [  \'attr\' => trim($m[2]),  \'value\' => trim($m[3]) ]; } } unset($segment); return new class($segments, $filters, $index) { private $segments; private $filters; private $index; private $matchCount = 0; public function __construct($segments, $filters, $index) { $this->segments = $segments; $this->filters = $filters; $this->index = $index; } public function matches(string $currentPath, XMLReader $reader): bool { $currentSegments = explode(\'/\', $currentPath); // 路径长度不匹配 if (count($currentSegments) !== count($this->segments)) {  return false; } // 检查每段路径(支持通配符*) foreach ($this->segments as $i => $segment) {  if ($segment === \'*\') { continue; // 通配符匹配任意段  }  if ($currentSegments[$i] !== $segment) { return false;  } } // 检查属性筛选条件 foreach ($this->filters as $filter) {  $attrValue = $reader->getAttribute($filter[\'attr\']);  if ($attrValue !== $filter[\'value\']) { return false;  } } // 检查索引匹配(仅当指定了索引) if ($this->index !== null) {  $this->matchCount++;  return $this->matchCount - 1 === $this->index; } return true; } }; } /** * 构建当前元素的路径字符串(修复版) * @return string 路径字符串(如:root/parent/child) */ private function buildCurrentPath(): string { static $pathStack = []; if ($this->isElementNode()) { $pathStack[] = $this->reader->name; } elseif ($this->isEndElementNode()) { array_pop($pathStack); } return implode(\'/\', $pathStack); } /** * 复制节点到指定XMLWriter * @param XMLReader $reader 源读取器 * @param XMLWriter $writer 目标写入器 */ private function copyNodeToWriter(XMLReader $reader, XMLWriter $writer): void { switch ($reader->nodeType) { case self::NODE_ELEMENT: $writer->startElement($reader->name); // 复制属性 if ($reader->hasAttributes) {  $reader->moveToFirstAttribute();  do { $writer->writeAttribute($reader->name, $reader->value);  } while ($reader->moveToNextAttribute());  $reader->moveToElement(); } if ($reader->isEmptyElement) {  $writer->endElement(); } break; case self::NODE_END_ELEMENT: $writer->endElement(); break; case self::NODE_TEXT: $writer->text($reader->value); break; case self::NODE_CDATA: $writer->writeCData($reader->value); break; case self::NODE_COMMENT: $writer->writeComment($reader->value); break; case self::NODE_PI: $writer->writePI($reader->name, $reader->value); break; case self::NODE_WHITESPACE: $writer->text($reader->value); break; } } /** * 获取当前元素的所有属性 * @return array 属性数组(键为属性名,值为属性值) */ private function getAllAttributes(): array { $attrs = []; if ($this->reader->hasAttributes) { $this->reader->moveToFirstAttribute(); do { $attrs[$this->reader->name] = $this->reader->value; } while ($this->reader->moveToNextAttribute()); $this->reader->moveToElement(); } return $attrs; } // ------------------------------ 基础方法 ------------------------------ /** * 开始元素(带命名空间支持) * @param string $name 元素名 */ private function startElement(string $name): void { if (strpos($name, \':\') !== false) { [$prefix, $localName] = explode(\':\', $name, 2); if (isset($this->namespaces[$prefix])) { $this->writer->startElementNS($prefix, $localName, $this->namespaces[$prefix]); } else { $this->writer->startElement($name); } } else { $this->writer->startElement($name); } } /** * 带命名空间的元素开始标签写入(基于当前reader节点) */ private function startElementWithNamespace(): void { $this->startElement($this->reader->name); } /** * 写入元素(带命名空间支持) * @param string $name 元素名称 * @param string $value 元素值(前缀\'cdata:\'则自动包裹CDATA) * @param array $attributes 属性数组 */ private function writeElement(string $name, string $value = \'\', array $attributes = []): void { if (empty($name) && !empty($value)) { $this->writer->writeRaw($value); // 写入原始XML片段 return; } $this->startElement($name); $this->writeAttributes($attributes); // 处理CDATA值 if (strpos($value, \'cdata:\') === 0) { $this->writer->writeCData(substr($value, 5)); } elseif ($value !== \'\') { $this->writer->text($value); } $this->writer->endElement(); } /** * 写入属性数组 * @param array $attributes 属性数组 [属性名 => 值] */ private function writeAttributes(array $attributes): void { foreach ($attributes as $name => $value) { if (strpos($name, \':\') !== false) { [$prefix, $local] = explode(\':\', $name, 2); if (isset($this->namespaces[$prefix])) {  $this->writer->writeAttributeNS($prefix, $local, $this->namespaces[$prefix], $value); } else {  $this->writer->writeAttribute($name, $value); } } else { $this->writer->writeAttribute($name, $value); } } } /** * 从当前reader写入属性 */ private function writeAttributesFromReader(): void { $this->writeAttributes($this->getAllAttributes()); }/** * 通用节点处理(支持所有节点类型) */ private function handleNode(): void { switch ($this->reader->nodeType) { case self::NODE_ELEMENT: $this->startElementWithNamespace(); $this->writeAttributesFromReader(); if ($this->reader->isEmptyElement) {  $this->writer->endElement(); } break; case self::NODE_END_ELEMENT: $this->writer->endElement(); break; case self::NODE_TEXT: $this->writer->text($this->reader->value); break; case self::NODE_CDATA: $this->writer->writeCData($this->reader->value); break; case self::NODE_COMMENT: $this->writer->writeComment($this->reader->value); break; case self::NODE_PI: $this->writer->writePI($this->reader->name, $this->reader->value); break; case self::NODE_WHITESPACE: $this->writer->text($this->reader->value); break; } } /** * 处理整个XML文档 */ private function processXml(): void { while ($this->reader->read()) { $this->handleNode(); } } /** * 处理XML内容(通用方法) * @param string $xmlString XML内容 * @param callable $processor 处理器回调(返回false则停止解析) */ private function processXmlContent(string $xmlString, callable $processor): void { if (!$this->reader->XML($xmlString)) { throw new DocxProcessingException(\'Failed to parse XML content\'); } while ($this->reader->read() && $processor() !== false) { // 处理器控制流程 } $this->reader->close(); } /** * XML修改通用方法 * @param string $xmlString XML内容 * @param callable $modifier 修改器回调(返回true则跳过默认处理) * @return string 修改后的XML */ private function modifyXml(string $xmlString, callable $modifier): string { if (!$this->reader->XML($xmlString)) { throw new DocxProcessingException(\'Failed to parse XML content\'); } $this->writer->openMemory(); $currentPath = []; $depth = 0; while ($this->reader->read()) { // 更新当前路径和深度 if ($this->isElementNode()) { $currentPath[] = $this->reader->name; $depth++; } elseif ($this->isEndElementNode()) { array_pop($currentPath); $depth--; } // 执行修改器,判断是否跳过默认处理 $skipDefault = $modifier($this->writer, $currentPath, $depth); if ($skipDefault) { continue; } $this->handleNode(); } $this->reader->close(); return $this->writer->outputMemory(); } /** * 节点类型判断辅助方法 */ private function isElementNode(): bool { return $this->reader->nodeType === self::NODE_ELEMENT; } private function isEndElementNode(): bool { return $this->reader->nodeType === self::NODE_END_ELEMENT; } private function isTextNode(): bool { return in_array($this->reader->nodeType, [self::NODE_TEXT, self::NODE_CDATA]); } /** * 析构函数 - 确保资源正确释放(修复语法错误) */ public function __destruct() { if (isset($this->reader) && $this->reader->nodeType !== XMLReader::NONE) { $this->reader->close(); } }}