用PHP生成XML文件通常有两条路:一是把整棵XML树加载到内存里构建,二是用流式写入器逐步输出。两者没有绝对优劣,关键在于数据规模和是否需要频繁调整结构。本文先演示DOMDocument的用法,再介绍XMLWriter的处理方式,最后讨论字符串拼接方案的坑点与浏览器输出细节。

一、DOMDocument:把XML当树来构建
DOMDocument是PHP内置的DOM扩展,它允许你像操作树一样创建节点、挂载子节点、写入属性。这套API虽然略显啰嗦,但结构清晰,尤其适合中小型XML文档,或者在生成后还要继续修改节点的场景。使用前需要确认php.ini中已经开启dom扩展,目前大多数PHP发行版都会默认启用。
下面这段代码会创建一个简单的商品列表,并保存到products.xml。节点创建、属性设置和CDATA写入都包含在内,基本覆盖日常导出需求。
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$dom->preserveWhiteSpace = false;
$root = $dom->createElement('products');
$dom->appendChild($root);
$product = $dom->createElement('product');
$product->setAttribute('id', '1001');
$name = $dom->createElement('name', '机械键盘');
$price = $dom->createElement('price', '299.00');
$product->appendChild($name);
$product->appendChild($price);
$desc = $dom->createCDATASection('支持热插拔 & 自定义灯效');
$product->appendChild($desc);
$root->appendChild($product);
$dom->save('products.xml');
echo $dom->saveXML();
这段代码先生成根节点products,再为每个商品创建product子节点。setAttribute用来写入id属性,createCDATASection适合描述文本中含有特殊符号的情况,比如这里的与号不会破坏XML结构。formatOutput设为true后,生成的XML会带缩进,便于阅读;save方法直接写文件,saveXML则返回字符串,方便接口返回或继续处理。
DOMDocument的一个明显优势是自动转义。如果你把用户昵称直接传给createElement或createTextNode,即便昵称里包含小于号、大于号或与号,输出的XML也会保持合法。唯一需要注意的是,在构建过程中节点全部驻留内存,当你要导出数十万条记录时,内存峰值会明显上升,这时更适合使用XMLWriter。
二、XMLWriter:流式生成更省内存
XMLWriter采用流式写入方式,边生成边输出,不会把整份文档同时放进内存。它在处理几万条甚至几十万条记录时非常实用,尤其适合订单导出、日志归档等批量任务。写法上需要手动管理开始和结束标签,如果不小心漏掉endElement,输出的XML就会缺闭合节点。
下面的例子导出一万名用户,每个用户节点包含id属性和昵称、积分两个子节点。整个过程内存占用基本保持稳定。
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->setIndent(true);
$writer->setIndentString(' ');
$writer->startElement('users');
for ($i = 1; $i <= 10000; $i++) {
$writer->startElement('user');
$writer->writeAttribute('id', $i);
$writer->writeElement('nickname', '用户' . $i);
$writer->writeElement('score', $i * 10);
$writer->endElement();
}
$writer->endElement();
$writer->endDocument();
file_put_contents('users.xml', $writer->outputMemory());
echo '生成完成';
openMemory表示先把内容写入内存缓冲区,最后通过outputMemory一次性取出。如果数据量更大,也可以用openUri直接写入文件路径,减少内存中转。setIndent开启缩进,setIndentString可以控制缩进字符,这里用四个空格,方便查看层级。
对比DOMDocument,XMLWriter的代码更紧凑,性能通常也更好,但缺点是可读性略低,你需要在脑中维护标签栈。对于需要频繁查找、修改某个节点的场景,XMLWriter并不合适,因为它是单向写入,不能回退到已经写出的节点。
三、字符串拼接虽然直接,但别忽略转义和编码
有些场景下,XML结构非常简单,团队里有人可能直接用字符串拼接来生成。这种方式上手快,能少写很多对象操作,但风险在于特殊字符没有转义时,会生成非法XML。比如商品标题包含小于号或与号,下游解析器就可能直接报错。
如果确实要用拼接方式,建议先封装一个转义函数,并统一使用UTF-8编码。下面是一个可运行的示例,注意htmlspecialchars的第三个参数必须显式指定UTF-8,否则在部分环境中可能因为默认编码不一致出现乱码。
function xmlEscape($value) {
return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
}
$items = [
['title' => 'AT&T 套餐', 'price' => '39.9'],
['title' => '5G < 无限流量', 'price' => '99.0'],
];
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$xml .= "<plans>\n";
foreach ($items as $item) {
$xml .= " <plan>\n";
$xml .= " <title>" . xmlEscape($item['title']) . "</title>\n";
$xml .= " <price>" . xmlEscape($item['price']) . "</price>\n";
$xml .= " </plan>\n";
}
$xml .= "</plans>";
file_put_contents('plans.xml', $xml);
注意上面字符串里的XML声明和标签都做了实体转义,这是为了让代码块在HTML页面中正常显示。自己实际编写脚本时,写成正常的尖括号即可。xmlEscape函数会把小于号转成<,大于号转成>,与号转成&,从而保证文本内容不会干扰XML结构。
不过字符串拼接始终依赖人工维护标签顺序,结构复杂后很容易漏掉闭合标签。只要项目允许使用DOM扩展,更推荐DOMDocument或XMLWriter,让底层帮你处理转义和标签配对。
四、输出到浏览器与常见问题排查
如果XML不是保存到服务器文件,而是直接提供给客户端下载,一定要在输出内容前设置响应头。常见做法是设置Content-Type为application/xml,并使用Content-Disposition提示浏览器下载。下面这段代码用DOMDocument构建一个根节点并输出,实际项目中把节点构建完整即可。
header('Content-Type: application/xml; charset=utf-8');
header('Content-Disposition: attachment; filename="products.xml"');
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElement('root');
$dom->appendChild($root);
echo $dom->saveXML();
文件生成后最常见的两个问题,一是XML声明没有出现在文档最开头,二是有UTF-8 BOM头。声明不在开头通常是因为在echo或save之前已经输出了空格、换行或者PHP文件开头的不可见字符。BOM问题则常见于用Windows记事本保存PHP文件后,文件头部被自动加入了三字节BOM,解析器会因此报错。所以PHP文件本身保存为无BOM的UTF-8格式很重要。
如果需要生成带命名空间的文档,DOMDocument和XMLWriter也都支持。下面以DOMDocument为例,创建一个带前缀的根节点。代码中的命名空间地址只是演示,实际使用时替换成自己的命名空间URI。
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElementNS('http://ipipp.com/ns', 'ns:products');
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns', 'http://ipipp.com/ns');
$dom->appendChild($root);
echo $dom->saveXML();
掌握这两种官方扩展后,动态创建XML文档就变得可控。小型、需要反复修改的结构优先用DOMDocument;导出量大、结构线性时换XMLWriter;即使偶尔用字符串拼接,也别忘了做好转义和编码检查。把这几条路径选对,PHP生成XML文件并不复杂。

