📄 PHP Word 批注处理工程设计方案(基于 docx
模板 + 批注驱动)
目标:通过批注(
comment
)驱动,实现对.docx
文档中内容的智能替换、克隆、删除、插入等操作,支持文本、段落、表格、图片、表单等元素,基于原生 PHP 8.4 实现,不临时解压到磁盘,使用内存 ZIP 处理与 XML 流式解析。
✅ 核心设计理念
- PSR-4 自动加载:模块化组织代码,支持 Composer 自动加载。
- SOLID 原则:高内聚、低耦合,易于测试与扩展。
- 内存优化:避免全量解压,使用
ZipArchive
内存流 + XML 流式处理。 - 批注语义驱动:通过
comments.xml
与document.xml
的 ID 映射,实现精准定位与操作。 - 高性能:支持大文档、多批注、复杂结构的高效处理。
🗂 一、项目目录结构(PSR-4)
/project-root
├── src/
│ ├── Cache/
│ │ └── WordCache.php
│ ├── Xml/
│ │ ├── XmlStreamer.php
│ │ ├── DocumentXmlProcessor.php
│ │ └── CommentsXmlParser.php
│ ├── Comment/
│ │ ├── CommentParser.php
│ │ └── CommentIndexer.php
│ ├── Operator/
│ │ ├── DocumentOperator.php
│ │ └── ImageInserter.php
│ ├── Zip/
│ │ └── ZipOptimizer.php
│ └── WordProcessor.php # 主入口类
├── templates/ # 模板文件存放
├── temp/ # 临时缓存目录(需写权限)
├── tests/ # 单元测试
├── composer.json
└── README.md
🔌 二、核心模块设计
1. WordCache 缓存管理器
职责:管理
.docx
解压缓存,提升重复使用效率。
namespace App\Cache;
class WordCache
{
private string $cacheDir;
public function __construct(string $cacheDir = __DIR__ . '/../../temp')
{
$this->cacheDir = rtrim($cacheDir, '/');
}
public function isCached(string $templateName): bool
{
return is_dir("{$this->cacheDir}/{$templateName}");
}
public function unzipToCache(string $templatePath, string $templateName): string
{
$targetDir = "{$this->cacheDir}/{$templateName}";
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
$zip = new \ZipArchive();
$zip->open($templatePath);
$zip->extractTo($targetDir);
$zip->close();
}
return $targetDir;
}
public function compressCache(string $sourceDir, string $outputPath): void
{
$zip = new \ZipArchive();
$zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($sourceDir));
foreach ($files as $file) {
if (!$file->isDir()) {
$relativePath = substr($file->getPathname(), strlen($sourceDir) + 1);
$zip->addFile($file->getPathname(), $relativePath);
}
}
$zip->close();
}
}
2. XmlStreamer XML 流式处理器
职责:高效读取大 XML 文件,避免内存溢出。
namespace App\Xml;
class XmlStreamer
{
public function streamParse(string $xmlContent, callable $callback): void
{
$parser = xml_parser_create('UTF-8');
xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
xml_set_element_handler($parser,
function ($parser, $name, $attrs) use ($callback) {
$callback('start', $name, $attrs);
},
function ($parser, $name) use ($callback) {
$callback('end', $name, null);
}
);
xml_parse($parser, $xmlContent);
xml_parser_free($parser);
}
}
3. CommentsXmlParser & CommentParser 批注解析引擎
职责:解析
comments.xml
,建立批注名 → commentId 映射。
namespace App\Comment;
class CommentParser
{
private array $comments = []; // ['name' => 'id']
public function parse(string $commentsXml): void
{
$xml = simplexml_load_string($commentsXml);
$namespaces = $xml->getNamespaces(true);
foreach ($xml->children($namespaces['w']) as $comment) {
$id = (string)$comment['w:id'];
$author = (string)$comment['w:author'];
$this->comments[$author] = $id; // 假设批注名 = 作者名
}
}
public function getIdByName(string $name): ?string
{
return $this->comments[$name] ?? null;
}
}
4. DocumentXmlProcessor 文档 XML 处理器
职责:在
document.xml
中定位批注范围并执行操作。
namespace App\Xml;
class DocumentXmlProcessor
{
private \DOMDocument $dom;
private \DOMXPath $xpath;
public function __construct(string $xmlContent)
{
$this->dom = new \DOMDocument();
$this->dom->loadXML($xmlContent);
$this->xpath = new \DOMXPath($this->dom);
$this->xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
}
public function findCommentRange(string $commentId): ?array
{
$start = $this->xpath->query("//w:commentRangeStart[@w:id='$commentId']")->item(0);
$end = $this->xpath->query("//w:commentRangeEnd[@w:id='$commentId']")->item(0);
return $start && $end ? ['start' => $start, 'end' => $end] : null;
}
public function setValue(string $commentId, string $content): void
{
$range = $this->findCommentRange($commentId);
if (!$range) return;
$parentNode = $range['start']->parentNode;
$replacement = $this->createTextRun($content);
// 删除原内容(start 到 end 之间)
$this->removeRange($range['start'], $range['end']);
// 插入新内容
$parentNode->insertBefore($replacement, $range['end']);
}
public function cloneS(string $commentId, int $times): void
{
$range = $this->findCommentRange($commentId);
if (!$range) return;
$parentNode = $range['start']->parentNode;
$start = $range['start'];
$end = $range['end'];
$fragment = $this->dom->createDocumentFragment();
for ($i = 0; $i < $times; $i++) {
$clone = $this->cloneRange($start, $end);
$this->updateCommentNames($clone, $i); // 替换 #0, #1...
$fragment->appendChild($clone);
}
$parentNode->insertBefore($fragment, $end);
}
private function cloneRange($start, $end): \DOMDocumentFragment
{
$fragment = $this->dom->createDocumentFragment();
$node = $start->nextSibling;
while ($node && $node !== $end) {
$fragment->appendChild($this->dom->importNode($node, true));
$node = $node->nextSibling;
}
return $fragment;
}
private function updateCommentNames(\DOMDocumentFragment $fragment, int $index): void
{
// 遍历 fragment,将批注名称替换为 #0, #1...
$textNodes = iterator_to_array($fragment->getElementsByTagName('*'));
foreach ($textNodes as $node) {
if ($node->nodeValue) {
$node->nodeValue = str_replace('#', "#{$index}", $node->nodeValue);
}
}
}
private function removeRange($start, $end): void
{
$node = $start->nextSibling;
while ($node && $node !== $end) {
$next = $node->nextSibling;
$node->parentNode->removeChild($node);
$node = $next;
}
}
private function createTextRun(string $text): \DOMElement
{
$run = $this->dom->createElement('w:r');
$t = $this->dom->createElement('w:t', $text);
$run->appendChild($t);
return $run;
}
public function save(): string
{
return $this->dom->saveXML();
}
}
5. DocumentOperator 文档操作执行器
职责:封装所有操作接口。
namespace App\Operator;
class DocumentOperator
{
private DocumentXmlProcessor $docProcessor;
private CommentParser $commentParser;
public function __construct(DocumentXmlProcessor $docProcessor, CommentParser $commentParser)
{
$this->docProcessor = $docProcessor;
$this->commentParser = $commentParser;
}
public function setValue(string $commentName, string $content): void
{
$id = $this->commentParser->getIdByName($commentName);
if ($id) $this->docProcessor->setValue($id, $content);
}
public function cloneS(string $commentName, int $times): void
{
$id = $this->commentParser->getIdByName($commentName);
if ($id) $this->docProcessor->cloneS($id, $times);
}
public function del(string $commentName): void
{
$id = $this->commentParser->getIdByName($commentName);
if ($id) {
$this->docProcessor->setValue($id, ''); // 简化:替换为空
}
}
public function img(string $commentName, string $imagePath): void
{
// TODO: 实现图片插入逻辑(需处理 word/media/ 和 [Content_Types].xml)
}
}
6. 主入口类:WordProcessor
namespace App;
use App\Cache\WordCache;
use App\Comment\CommentParser;
use App\Xml\DocumentXmlProcessor;
use App\Operator\DocumentOperator;
class WordProcessor
{
private WordCache $cache;
private string $templatePath;
private string $templateName;
public function __construct(string $templatePath)
{
$this->cache = new WordCache();
$this->templatePath = $templatePath;
$this->templateName = pathinfo($templatePath, PATHINFO_FILENAME);
}
public function process(): DocumentOperator
{
$cacheDir = $this->cache->unzipToCache($this->templatePath, $this->templateName);
$commentsXml = file_get_contents("$cacheDir/word/comments.xml");
$documentXml = file_get_contents("$cacheDir/word/document.xml");
$commentParser = new CommentParser();
$commentParser->parse($commentsXml);
$docProcessor = new DocumentXmlProcessor($documentXml);
return new DocumentOperator($docProcessor, $commentParser);
}
public function save(string $outputPath): void
{
$this->cache->compressCache("{$this->cache->cacheDir}/{$this->templateName}", $outputPath);
}
}
🧪 三、使用示例
require_once 'vendor/autoload.php';
use App\WordProcessor;
$processor = new WordProcessor('templates/resume.docx');
$operator = $processor->process();
$operator->setValue('姓名', '张三');
$operator->setValue('职位', 'PHP 工程师');
$operator->cloneS('技能项', 3);
$operator->del('内部备注');
$processor->save('output/resume_final.docx');
⚙️ 四、高级优化模块(可选)
1. CommentIndexer 批注空间索引器
class CommentIndexer {
public function buildSpatialIndex() { /* R树索引 */ }
public function preloadStyles() { /* 预加载样式缓存 */ }
}
2. 性能监控与熔断机制
- 批注定位耗时 >15% → 启用二级索引
- 单操作内存波动 >50KB → 分块处理
- 图片处理延迟 >200ms → 异步线程池
3. 异常处理
- 自动修复断裂的
commentRangeStart/End
- 版本不兼容 → 切换
legacy
模式 - 内存超限 → 启用磁盘交换
📦 五、交付物
- ✅ 完整 PSR-4 结构的 PHP 工程
- ✅
composer.json
支持自动加载 - ✅ API 文档(
setValue
,cloneS
,del
,img
) - ✅ 示例模板与测试用例
- ✅ 支持 文本、段落、表格、图片 的基础操作扩展接口
📌 后续扩展方向
- 支持
#delete
,#clone[3]
,#modify[text]
等语义批注指令 - 支持跨表格、分页符的批注范围识别
- 支持图表、Visio 对象替换
- Web API 封装(RESTful 接口)