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

PhpWord合并docx时如何复制样式与编号到目标文件

Word合并后项目符号编号显示异常修复

你的判断是对的:当前合并逻辑仅同步了正文内容、媒体资源、评论数据,缺失源文件的编号定义组件,导致正文里引用的编号规则找不到对应配置,才会出现显示异常。
生成文件正常效果
目标文件异常效果
以下是针对你贴的generate.php代码的具体修改方案:

核心问题原因

OOXML格式的docx文件中,所有列表、项目符号的编号规则都存在word/numbering.xml文件里,正文仅通过w:numId标签引用对应规则ID。你当前的代码没有处理这个文件,也没有补全这个文件对应的包关联、内容类型声明,Word打开时找不到编号定义就会渲染失败。

具体修改步骤

    1. 同步源文件的numbering.xml到目标包
      现有遍历源压缩包复制资源的逻辑,仅匹配了media目录和comments.xml,新增numbering.xml的匹配规则即可:
    // 原有遍历sourceZip文件的循环,修改判断条件
    for ($i = 0; $i < $sourceZip->numFiles; $i++) {
        $currentFileName = $sourceZip->getNameIndex($i);
        // 新增numbering.xml的判断
        if(strpos($currentFileName, 'media') !== false 
            || strpos($currentFileName, 'comments.xml') !== false 
            || strpos($currentFileName, 'numbering.xml') !== false){
            if(strpos($currentFileName, 'comments.xml') !== false)
                $ifThereAreComments = 1;
            
            // 原有临时文件写入逻辑不变
            $fileContent = $sourceZip->getFromName($currentFileName);
            $temp = fopen(TEMPORARYFILES.substr(bin2hex(random_bytes(5)),0, 5), "w");
            fwrite($temp, $fileContent);  
            fseek($temp, 0);
            $targetZip -> addFile(stream_get_meta_data($temp)['uri'], $currentFileName);
        }
    }
    
    如果你的模板文件本身已经自带numbering.xml(即模板预设了编号规则),不要直接覆盖,需要解析两个numbering.xml的内容,将源文件的numId、abstractNumId做偏移后合并到目标文件的numbering.xml中,避免ID冲突覆盖模板原有规则。空模板直接复制即可。
    1. 补全document.xml的关联关系
      在现有添加评论关联、图片关联的逻辑段后,新增numbering.xml的关联声明,注意使用未被占用的关系ID:
    // 放在if($ifThereAreComments)逻辑块之后即可
    $targetRelationsDom = new DOMDocument();
    $targetRelationsDom->loadXML($targetRelationsDocument);
    $relParentNode = $targetRelationsDom -> getElementsByTagName('Relationships')->item(0);
    
    // 新增numbering关系
    $numberingRelNode = $targetRelationsDom->createElement("Relationship");
    $idAttr = $targetRelationsDom->createAttribute('Id');
    $idAttr->value = 'rId1001'; // 确保这个ID没被其他关系占用
    $numberingRelNode->appendChild($idAttr);
    $targetAttr = $targetRelationsDom->createAttribute('Target');
    $targetAttr->value = 'numbering.xml';
    $numberingRelNode->appendChild($targetAttr);
    $typeAttr = $targetRelationsDom->createAttribute('Type');
    $typeAttr->value = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
    $numberingRelNode->appendChild($typeAttr);
    $relParentNode->appendChild($numberingRelNode);
    
    $targetZip->addFromString('word/_rels/document.xml.rels', $targetRelationsDom->saveXML());
    
    1. 补全Content_Types声明
      和comments.xml的类型声明逻辑一致,新增numbering.xml的内容类型:
    $typeParentNode = $targetContentTypesDom -> getElementsByTagName('Types')->item(0);
    $numberingTypeNode = $targetContentTypesDom->createElement("Override");
    $partAttr = $targetContentTypesDom->createAttribute('PartName');
    $partAttr->value = '/word/numbering.xml';
    $numberingTypeNode->appendChild($partAttr);
    $ctAttr = $targetContentTypesDom->createAttribute('ContentType');
    $ctAttr->value = 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml';
    $numberingTypeNode->appendChild($ctAttr);
    $typeParentNode->appendChild($numberingTypeNode);
    
    $targetZip->addFromString('[Content_Types].xml', $targetContentTypesDom->saveXML());
    

扩展说明

如果后续合并后出现自定义样式显示异常,用完全相同的逻辑处理word/styles.xml即可:复制文件、补document.xml.rels关联、补Content_Types声明。如果模板自带styles.xml,同样需要做ID偏移合并,不要直接覆盖。

原有问题代码参考

if(!empty($newresult['header']) && !empty($result['snapshot']))
                {
                    \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true);
                    $phpWord = new \PhpOffice\PhpWord\PhpWord();
                    $section = $phpWord->addSection();
                    
                    if(!empty($result['snapshot']))
                    {
                        $decoded = json_decode($result['snapshot'], true);
                        for($i = sizeof($decoded['blocks']) - 1; $i>= 0; $i--){
                            if($decoded['blocks'][$i]['text'] != '')
                                break;
                            else{
                                array_pop($decoded['blocks']);
                            }
                            
                        }
                        makeWordStrings($section, json_decode($newresult['snapshot'], true), $demographics, $decoded, $tabSize, 0);
                    } 
                    
                    $newfilename = $filename;
                    if(mysqli_num_rows($query) > 1){
                        if($result['file_part_number'] > 1)
                            $newfilename .= '_'.(strlen($result['file_part_number']-1)==1?'0'.($result['file_part_number']-1):($result['file_part_number']-1));
                        
                    }
                    $phpWord->save(WITHOUTTEMPLATE."$newfilename.docx", "Word2007");
                    
                    $templateFile  = TEMPLATES.$newresult['header'];
                    $generatedFile = WITHOUTTEMPLATE."$newfilename.docx";
                    
                    $targetFile    = $storageDirectory.($includeNoName==1?"":"$newfilename.docx");
                     
                    // copy template to target
                    copy($templateFile, $targetFile);
                    
                    
                    // open target
                    $targetZip = new ZipArchive();
                    $targetZip->open($targetFile);
                    $targetDocument = $targetZip->getFromName('word/document.xml');
                    $targetRelationsDocument = $targetZip->getFromName('word/_rels/document.xml.rels');
                    $targetCorePropsDocument = $targetZip->getFromName('docProps/core.xml');
                    $targetCustomPropsDocument = $targetZip->getFromName('docProps/custom.xml');
                    $targetContentTypesDocument = $targetZip->getFromName('[Content_Types].xml');
                    
                    $targetDom      = new DOMDocument();
                    $targetDom->loadXML($targetDocument);

                    $targetCorePropsDom = new DOMDocument();
                    $targetCorePropsDom->loadXML($targetCorePropsDocument);
                    
                    
                    $targetContentTypesDom = new DOMDocument();
                    $targetContentTypesDom->loadXML($targetContentTypesDocument);
                    
                    $targetXPath = new \DOMXPath($targetDom);
                    $targetXPath->registerNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
                    
                     
                    // open source
                    $sourceZip = new ZipArchive();
                    $sourceZip->open($generatedFile);
                    $sourceDocument = $sourceZip->getFromName('word/document.xml');
                    $sourceRelationsDocument = $sourceZip->getFromName('word/_rels/document.xml.rels');
                    $sourceContentTypesDocument = $sourceZip->getFromName('[Content_Types].xml');

                    $sourceDom      = new DOMDocument();
                    $sourceDom->loadXML($sourceDocument);
                    
                    $sourceRelationsDom = new DOMDocument();
                    $sourceRelationsDom->loadXML($sourceRelationsDocument);

                    $sourceContentTypes = new DOMDocument();
                    $sourceContentTypes->loadXML($sourceContentTypesDocument);

                    $sourceXPath = new \DOMXPath($sourceDom);
                    $sourceXPath->registerNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
                    
                    $sourceRelationsXPath = new \DOMXPath($sourceRelationsDom);
                     
                    $replacementMarkerNode = $targetXPath->query('//w:p[contains(translate(normalize-space(), " ", ""),"$CONTENT$")]')[0];

                    // insert source nodes before the replacement marker
                    $sourceNodes = $sourceXPath->query('//w:document/w:body/*[not(self::w:sectPr)]');
                    
                    $relationsArr = array();
                    $startId = 700;
                    
                    $ifThereAreComments = 0;
                    
                    for ($i = 0; $i < $sourceZip->numFiles; $i++) {
                        if(strpos($sourceZip->getNameIndex($i), 'media') > 0 || strpos($sourceZip->getNameIndex($i), 'comments.xml') > 0){
                            if(strpos($sourceZip->getNameIndex($i), 'comments.xml') > 0)
                                $ifThereAreComments = 1;
                            
                            $imagefile = $sourceZip->getFromName($sourceZip->getNameIndex($i));
                            $temp = fopen(TEMPORARYFILES.substr(bin2hex(random_bytes(5)),0, 5), "w");
                            fwrite($temp, $imagefile);  
                            fseek($temp, 0);
                            
                            $targetZip -> addFile(stream_get_meta_data($temp)['uri'], $sourceZip->getNameIndex($i));
                        }
                            
                    }                   
                    
                    
                    foreach ($sourceNodes as $sourceNode) {
                        $checkifImage = $sourceNode -> getElementsByTagNameNS ("urn:schemas-microsoft-com:vml", 'imagedata');
                        foreach($checkifImage AS $t)
                        {
                            $rid = $t->getAttribute('r:id');
                            $relationsArr[$rid] = array('id'=>'rId'.$startId, 'media'=>'');
                            $t->setAttribute('r:id', $relationsArr[$rid]['id']);
                            $startId++;
                        }
                        
                        $imported = $replacementMarkerNode->ownerDocument->importNode($sourceNode, true);
                        $inserted = $replacementMarkerNode->parentNode->insertBefore($imported, $replacementMarkerNode);
                    }
                    
                    // remove $replacementMarkerNode from the target DOM
                    $replacementMarkerNode->parentNode->removeChild($replacementMarkerNode); 
                      
                    // save target
                    $targetZip->addFromString('word/document.xml', $targetDom->saveXML());
                    
                    $getSourceRelationsTree = $sourceRelationsDom -> getElementsByTagName('Relationship');
                    foreach($getSourceRelationsTree AS $t){
                        $rid = $t->getAttribute('Id');
                        if(isset($relationsArr[$rid])){
                            $relationsArr[$rid]['media'] = $t->getAttribute('Target');
                        }
                    }
                    
                    if($ifThereAreComments){
                        $targetRelationsDom = new DOMDocument();
                        $targetRelationsDom->loadXML($targetRelationsDocument);
                    
                        $parentDom = $targetRelationsDom -> getElementsByTagName('Relationships');
                        $relationsElement = $targetRelationsDom->createElement("Relationship");
                        
                        $domAttribute = $targetRelationsDom->createAttribute('Id');
                        $domAttribute->value = 'rId1000';
                        $relationsElement->appendChild($domAttribute);
                        
                        $domAttribute = $targetRelationsDom->createAttribute('Target');
                        $domAttribute->value = 'comments.xml';
                        $relationsElement->appendChild($domAttribute);
                        $domAttribute = $targetRelationsDom->createAttribute('Type');
                        $domAttribute->value = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
                        $relationsElement->appendChild($domAttribute);
                        
                        $parentDom[0]->appendChild($relationsElement);
                        
                        $targetZip->addFromString('word/_rels/document.xml.rels', $targetRelationsDom->saveXML());
                        
                        
                        $parentDom = $targetContentTypesDom -> getElementsByTagName('Types');
                        $relationsElement = $targetContentTypesDom->createElement("Override");
                        
                        $domAttribute = $targetContentTypesDom->createAttribute('PartName');
                        $domAttribute->value = '/word/comments.xml';
                        $relationsElement->appendChild($domAttribute);
                        
                        $domAttribute = $targetContentTypesDom->createAttribute('ContentType');
                        $domAttribute->value = 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml';
                        $relationsElement->appendChild($domAttribute);
                        
                        $parentDom[0]->appendChild($relationsElement);
                        
                        $targetZip->addFromString('[Content_Types].xml', $targetContentTypesDom->saveXML());                        
                        
                    }
                    
                    if(!empty($relationsArr)){
                        $targetRelationsDom = new DOMDocument();
                        $targetRelationsDom->loadXML($targetRelationsDocument);
                    
                        $parentDom = $targetRelationsDom -> getElementsByTagName('Relationships');
                        foreach($relationsArr AS $t){
                            $relationsElement = $targetRelationsDom->createElement("Relationship");
                            
                            $domAttribute = $targetRelationsDom->createAttribute('Id');
                            $domAttribute->value = $t['id'];
                            $relationsElement->appendChild($domAttribute);
                            
                            $domAttribute = $targetRelationsDom->createAttribute('Target');
                            $domAttribute->value = $t['media'];
                            $relationsElement->appendChild($domAttribute);
                            
                            $domAttribute = $targetRelationsDom->createAttribute('Type');
                            $domAttribute->value = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
                            $relationsElement->appendChild($domAttribute);
                            
                            $parentDom[0]->appendChild($relationsElement);
                        }
                        
                        $targetZip->addFromString('word/_rels/document.xml.rels', $targetRelationsDom->saveXML());
                    }
                    
                    $parentDom = $targetCorePropsDom -> getElementsByTagNameNS("http://schemas.openxmlformats.org/package/2006/metadata/core-properties", 'coreProperties');
                    $toRem = $parentDom[0]->getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", 'title')->Item(0);
                    $relationsElement = $targetCorePropsDom->createElement("dc:title", isset($demographics['{{title}}'])?$demographics['{{title}}']['value']:null);
                        
                    $parentDom[0]->appendChild($relationsElement);
                    $toRem->parentNode->removeChild($toRem);
                    
                    $toRem = $parentDom[0]->getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", 'subject')->Item(0);
                    $relationsElement = $targetCorePropsDom->createElement("dc:subject", $dict_id);
                        
                    $parentDom[0]->appendChild($relationsElement);
                    $toRem->parentNode->removeChild($toRem);
                    
                    
                    $targetZip->addFromString('docProps/core.xml', $targetCorePropsDom->saveXML());

                    
                    $targetZip->close();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 19:01:29