XML DOM
Loading XML as a node tree, traversing and modifying it, and generating XML.
Reading XML
<?php
$xml = '<?xml version="1.0" encoding="UTF-8"?>
<books>
<book id="1"><title>PHP 入门</title><price>59</price></book>
<book id="2"><title>PHP 进阶</title><price>79</price></book>
</books>';
$doc = new DOMDocument();
$doc->loadXML($xml);
$books = $doc->getElementsByTagName("book");
foreach ($books as $b) {
$title = $b->getElementsByTagName("title")->item(0)->nodeValue;
$price = $b->getElementsByTagName("price")->item(0)->nodeValue;
echo $b->getAttribute("id") . " " . $title . " " . $price . "\n";
}
echo "共 " . $books->length . " 本\n";
echo "根元素: " . $doc->documentElement->nodeName . "\n";1 PHP 入门 59
2 PHP 进阶 79
共 2 本
根元素: booksXPath
getElementsByTagName filters by name only. For attributes, positions and paths, use XPath.
<?php
$doc = new DOMDocument();
$doc->loadXML($xml);
$xp = new DOMXPath($doc);
echo $xp->query("/books/book[@id='2']/title")->item(0)->nodeValue, "\n";
echo $xp->query("//book[price > 60]/title")->item(0)->nodeValue, "\n";
echo $xp->evaluate("count(//book)"), "\n";
echo $xp->evaluate("sum(//price)"), "\n";PHP 进阶
PHP 进阶
2
138query() returns a node set; evaluate() returns a scalar. Mixing them yields empty results rather than errors.
Building XML
<?php
$doc = new DOMDocument("1.0", "UTF-8");
$doc->formatOutput = true;
$root = $doc->createElement("books");
$doc->appendChild($root);
foreach ([["1", "PHP 入门", 59], ["2", "PHP 进阶", 79]] as [$id, $title, $price]) {
$book = $doc->createElement("book");
$book->setAttribute("id", $id);
$t = $doc->createElement("title");
$t->appendChild($doc->createTextNode($title));
$book->appendChild($t);
$pr = $doc->createElement("price", (string)$price);
$book->appendChild($pr);
$root->appendChild($book);
}
echo $doc->saveXML();<?xml version="1.0" encoding="UTF-8"?>
<books>
<book id="1">
<title>PHP 入门</title>
<price>59</price>
</book>
<book id="2">
<title>PHP 进阶</title>
<price>79</price>
</book>
</books>createElement's second argument becomes text, but any < or & needs createTextNode. **Always use createTextNode for user data.**Modifying XML
<?php
$doc = new DOMDocument();
$doc->loadXML($xml);
$doc->formatOutput = true;
$xp = new DOMXPath($doc);
// 改文本
$xp->query("//book[@id='1']/price")->item(0)->nodeValue = "49";
// 加属性
$xp->query("//book[@id='2']")->item(0)->setAttribute("stock", "10");
// 删节点
$del = $xp->query("//book[@id='2']/price")->item(0);
$del->parentNode->removeChild($del);
echo $doc->saveXML();<?xml version="1.0" encoding="UTF-8"?>
<books>
<book id="1">
<title>PHP 入门</title>
<price>49</price>
</book>
<book id="2" stock="10">
<title>PHP 进阶</title>
</book>
</books>Removal goes through the parent: $node->parentNode->removeChild($node).
From files and strings
<?php
$path = __DIR__ . "/data.xml";
file_put_contents($path, '<r><v>1</v></r>');
// 从文件
$doc = new DOMDocument();
$ok = $doc->load($path);
echo $ok ? "load 成功" : "load 失败", "\n";
echo $doc->getElementsByTagName("v")->item(0)->nodeValue, "\n";
// 保存到文件
$doc->save($path);
echo filesize($path) > 0 ? "已保存" : "保存失败", "\n";
unlink($path);load 成功
1
已保存load() spews warnings on failure. Call libxml_use_internal_errors(true) and read libxml_get_errors() yourself.<?php
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$ok = $doc->loadXML("<r><unclosed></r>");
if (!$ok) {
foreach (libxml_get_errors() as $e) {
printf("第 %d 行: %s", $e->line, trim($e->message) . "\n");
}
libxml_clear_errors();
}
echo $ok ? "解析成功" : "解析失败", "\n";第 1 行: Opening and ending tag mismatch: unclosed line 1 and r
解析失败Security: disable external entities
By default external entities may be resolved, enabling XXE — reading /etc/passwd via <!ENTITY xxe SYSTEM "file:///etc/passwd">. PHP 8 disables it by default; older versions need this explicit.
<?php
$doc = new DOMDocument();
// 解析用户上传的 XML:禁止网络加载
$ok = $doc->loadXML('<r><v>1</v></r>', LIBXML_NONET);
echo $ok ? "解析成功" : "解析失败", "\n";
echo $doc->getElementsByTagName("v")->item(0)->nodeValue, "\n";解析成功
1LIBXML_NONET blocks network loading. Before PHP 8 you also needed libxml_disable_entity_loader(true) to block file://; it is deprecated since 8.0 because the default is now safe. Mandatory for user-uploaded XML.读 XML
<?php
$xml = '<?xml version="1.0" encoding="UTF-8"?>
<books>
<book id="1"><title>PHP 入门</title><price>59</price></book>
<book id="2"><title>PHP 进阶</title><price>79</price></book>
</books>';
$doc = new DOMDocument();
$doc->loadXML($xml);
$books = $doc->getElementsByTagName("book");
foreach ($books as $b) {
$title = $b->getElementsByTagName("title")->item(0)->nodeValue;
$price = $b->getElementsByTagName("price")->item(0)->nodeValue;
echo $b->getAttribute("id") . " " . $title . " " . $price . "\n";
}
echo "共 " . $books->length . " 本\n";
echo "根元素: " . $doc->documentElement->nodeName . "\n";1 PHP 入门 59
2 PHP 进阶 79
共 2 本
根元素: booksXPath 查询
getElementsByTagName 只能按标签名找。要按属性、位置、层级筛选,用 XPath。
<?php
$doc = new DOMDocument();
$doc->loadXML($xml);
$xp = new DOMXPath($doc);
echo $xp->query("/books/book[@id='2']/title")->item(0)->nodeValue, "\n";
echo $xp->query("//book[price > 60]/title")->item(0)->nodeValue, "\n";
echo $xp->evaluate("count(//book)"), "\n";
echo $xp->evaluate("sum(//price)"), "\n";PHP 进阶
PHP 进阶
2
138query() 返回节点集,evaluate() 返回标量(count、sum、字符串)。用错了会得到空结果而不是报错。
生成 XML
<?php
$doc = new DOMDocument("1.0", "UTF-8");
$doc->formatOutput = true;
$root = $doc->createElement("books");
$doc->appendChild($root);
foreach ([["1", "PHP 入门", 59], ["2", "PHP 进阶", 79]] as [$id, $title, $price]) {
$book = $doc->createElement("book");
$book->setAttribute("id", $id);
$t = $doc->createElement("title");
$t->appendChild($doc->createTextNode($title));
$book->appendChild($t);
$pr = $doc->createElement("price", (string)$price);
$book->appendChild($pr);
$root->appendChild($book);
}
echo $doc->saveXML();<?xml version="1.0" encoding="UTF-8"?>
<books>
<book id="1">
<title>PHP 入门</title>
<price>59</price>
</book>
<book id="2">
<title>PHP 进阶</title>
<price>79</price>
</book>
</books>createElement("title", "PHP 入门") 的第二个参数是文本内容,会被当纯文本处理;但如果内容里有 < 或 &,要用 createTextNode 才安全。**用户数据一律走 createTextNode。**修改 XML
<?php
$doc = new DOMDocument();
$doc->loadXML($xml);
$doc->formatOutput = true;
$xp = new DOMXPath($doc);
// 改文本
$xp->query("//book[@id='1']/price")->item(0)->nodeValue = "49";
// 加属性
$xp->query("//book[@id='2']")->item(0)->setAttribute("stock", "10");
// 删节点
$del = $xp->query("//book[@id='2']/price")->item(0);
$del->parentNode->removeChild($del);
echo $doc->saveXML();<?xml version="1.0" encoding="UTF-8"?>
<books>
<book id="1">
<title>PHP 入门</title>
<price>49</price>
</book>
<book id="2" stock="10">
<title>PHP 进阶</title>
</book>
</books>删节点要**从父节点调 removeChild**,不能对节点自己调。$node->parentNode->removeChild($node) 是固定写法。
从文件与字符串
<?php
$path = __DIR__ . "/data.xml";
file_put_contents($path, '<r><v>1</v></r>');
// 从文件
$doc = new DOMDocument();
$ok = $doc->load($path);
echo $ok ? "load 成功" : "load 失败", "\n";
echo $doc->getElementsByTagName("v")->item(0)->nodeValue, "\n";
// 保存到文件
$doc->save($path);
echo filesize($path) > 0 ? "已保存" : "保存失败", "\n";
unlink($path);load 成功
1
已保存load() 失败时会往 PHP 输出里塞一堆 warning。用 libxml_use_internal_errors(true) 把警告收起来,再用 libxml_get_errors() 自己处理。<?php
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$ok = $doc->loadXML("<r><unclosed></r>");
if (!$ok) {
foreach (libxml_get_errors() as $e) {
printf("第 %d 行: %s", $e->line, trim($e->message) . "\n");
}
libxml_clear_errors();
}
echo $ok ? "解析成功" : "解析失败", "\n";第 1 行: Opening and ending tag mismatch: unclosed line 1 and r
解析失败安全:禁用外部实体
默认配置下,解析外部 XML 可能被 XXE 攻击——攻击者用 <!ENTITY xxe SYSTEM "file:///etc/passwd"> 读服务器文件。PHP 8 起默认不再加载外部实体,但老版本必须手动关。
<?php
$doc = new DOMDocument();
// 解析用户上传的 XML:禁止网络加载
$ok = $doc->loadXML('<r><v>1</v></r>', LIBXML_NONET);
echo $ok ? "解析成功" : "解析失败", "\n";
echo $doc->getElementsByTagName("v")->item(0)->nodeValue, "\n";解析成功
1LIBXML_NONET 禁止网络加载。PHP 8 之前还必须调 libxml_disable_entity_loader(true) 才能同时挡住 file:// 读取——这个函数在 8.0 起已弃用,因为默认行为已经安全。处理用户上传的 XML 时这是必做项。