在PHP中处理数据导出或接口报文时,经常需要生成结构严谨的XML文件。当XML包含多层嵌套、命名空间以及混合属性时,使用字符串拼接不仅容易出错,而且难以维护。XMLWriter扩展提供了一套基于流和栈的写入接口,能够以极低的内存开销逐步构建复杂文档。

XMLWriter基础写入流程
XMLWriter的核心思路是“打开文档、开始元素、写属性、写内容、结束元素”。它不要求一次性把整棵树加载到内存,而是像写文件一样按顺序输出。这种方式特别适合生成体积较大的XML,因为DOMDocument会把所有节点驻留内存,而XMLWriter可以边写边丢。
下面是一段最基础的示例,展示如何创建一个带有版本声明和单个元素的XML:
<?php
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('root');
$writer->writeAttribute('id', '1001');
$writer->text('hello world');
$writer->endElement();
$writer->endDocument();
echo $writer->outputMemory();
?>
上述代码会输出一个完整的XML文档。注意startElement和endElement必须成对出现,否则生成的文档会缺少闭合标签。如果某个节点没有子节点也没有文本,可以使用writeElement一次性写出自闭合或带内容的简单标签。
构建多层嵌套结构
复杂XML往往包含多层级。利用XMLWriter的栈式设计,只要保证开始和结束的顺序正确,就能写出任意深度。例如一个订单包含多个商品,每个商品又有规格子节点,可以通过循环嵌套实现。
在嵌套过程中,建议将每一层封装成独立方法,这样逻辑更清晰,也方便复用。以下示例演示订单报文的生成:
<?php
function buildOrderXml($order) {
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('order');
$writer->writeAttribute('sn', $order['sn']);
$writer->writeAttribute('status', $order['status']);
$writer->startElement('buyer');
$writer->writeAttribute('id', $order['buyer_id']);
$writer->text($order['buyer_name']);
$writer->endElement();
$writer->startElement('items');
foreach ($order['items'] as $item) {
$writer->startElement('item');
$writer->writeAttribute('sku', $item['sku']);
$writer->writeAttribute('qty', $item['qty']);
$writer->startElement('spec');
$writer->writeElement('color', $item['color']);
$writer->writeElement('size', $item['size']);
$writer->endElement();
$writer->endElement();
}
$writer->endElement();
$writer->endElement();
$writer->endDocument();
return $writer->outputMemory();
}
$order = array(
'sn' => 'NO20240501',
'status' => 'paid',
'buyer_id' => 88,
'buyer_name' => '张三',
'items' => array(
array('sku' => 'A1', 'qty' => 2, 'color' => '红', 'size' => 'L'),
array('sku' => 'B2', 'qty' => 1, 'color' => '蓝', 'size' => 'M')
)
);
echo buildOrderXml($order);
?>
这段代码中,items节点下通过foreach动态写入多个item,每个item内部又嵌套了spec。由于XMLWriter严格按照调用顺序输出,只要结束标签的顺序与开始标签相反,就能保证结构合法。
与DOM方式相比,这种写法在商品数量达到几万条时内存占用可以保持在极低水平,因为不需要把所有节点对象保存在内存中。但如果业务逻辑中需要频繁修改已写入的内容,XMLWriter就不合适,此时应改用DOMDocument。
处理属性与混合内容
真实场景里,节点往往同时拥有属性和文本,甚至文本中夹杂子元素。XMLWriter提供了writeAttribute写属性,text写纯文本,以及startElement继续子元素。要特别注意,如果先调用了text再想加子元素,必须保证文本已结束,否则会破坏格式。
当某个节点只有属性没有内容时,应使用endElement而不是fullEndElement的区别在于后者会强制输出完整闭合标签<node></node>,而前者在空节点时可能输出自闭合<node/>。以下示例展示混合内容:
<?php
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('message');
$writer->writeAttribute('type', 'warning');
$writer->text('系统将在');
$writer->startElement('time');
$writer->text('5分钟');
$writer->endElement();
$writer->text('后重启');
$writer->fullEndElement();
$writer->endDocument();
echo $writer->outputMemory();
?>
上面的message节点既有直接文本,又包含time子元素,使用fullEndElement确保它不以自闭合形式出现,符合某些旧系统的解析要求。
如果生成的XML需要被严格校验,建议在输出后使用DOMDocument::loadXML做一次合法性检查,或者开启XMLWriter的缩进功能:$writer->setIndent(true);让结构更易读,方便排查标签错位问题。
命名空间与支持字符
在对接第三方接口时,XML常带命名空间。XMLWriter通过startElementNS和writeAttributeNS支持带前缀的节点。需要注意URI参数必须准确,否则解析端会报未知命名空间错误。
另外,写入文本内容时如果包含<、>、&等字符,XMLWriter的text方法会自动转义,不需要手动处理。但如果使用writeRaw写入原始片段,就要自己保证合法性。示例如下:
<?php
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElementNS('ns', 'doc', 'http://ippipp.com/ns');
$writer->writeAttribute('lang', 'zh');
$writer->text('价格 < 100 且库存 > 0');
$writer->endElement();
$writer->endDocument();
echo $writer->outputMemory();
?>
该代码生成的节点带命名空间前缀ns:,且文本中的比较符号被正确处理。对于大多数业务系统,掌握上述写法已经能覆盖九成以上的复杂XML生成需求。
综合来看,PHP的XMLWriter以顺序写入、低内存和精细控制见长。只要理清节点层级,配对开始与结束标签,就能稳定产出符合规范的复杂结构文档,而不必受制于字符串拼接的混乱与DOM的高内存消耗。