Skip to content

DOM 操作

PHP 的 DOM 扩展实现了 W3C DOM 标准,提供了操作 XML 和 HTML 文档的完整 API。PHP 8.4 引入了现代化的 DOM 命名空间,使 API 更加面向对象和类型安全。本节将全面讲解 PHP DOM 操作的核心方法。

前置知识

阅读本节前,建议先了解:PHP 面向对象编程字符串处理

基础概念

什么是 DOM

DOM(Document Object Model,文档对象模型)将 XML/HTML 文档表示为树形结构,其中每个节点都是一个对象。PHP 的 DOM 扩展基于 libxml2 库,完全遵循 W3C DOM Level 2/3 标准。

DOM 树结构

Document
  |-- Element (html)
       |-- Element (head)
       |    |-- Element (title)
       |         |-- Text ("页面标题")
       |-- Element (body)
            |-- Element (div)
                 |-- Attribute (class="container")
                 |-- Text ("内容")

PHP 8.4 DOM 命名空间

PHP 8.4 将 DOM 类移至 DOM\ 命名空间下,提供了更好的类型安全和现代化 API:

php
<?php
declare(strict_types=1);

// PHP 8.4+ 新命名空间
$doc = new DOM\HTMLDocument();
$doc = new DOM\XMLDocument();
$element = $doc->createElement('div');

// 旧 API 仍可用(向后兼容)
$doc = new DOMDocument();

创建 XML 文档

从字符串加载

php
<?php
declare(strict_types=1);

$xmlString = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
    <book category="fiction">
        <title lang="en">Harry Potter</title>
        <author>J.K. Rowling</author>
        <year>2005</year>
        <price>29.99</price>
    </book>
    <book category="programming">
        <title lang="en">Clean Code</title>
        <author>Robert C. Martin</author>
        <year>2008</year>
        <price>39.99</price>
    </book>
</bookstore>
XML;

$doc = new DOMDocument();
$doc->loadXML($xmlString, LIBXML_NOBLANKS);

echo $doc->saveXML();

从文件加载

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->preserveWhiteSpace = false; // 去除空白节点
$doc->formatOutput = true;        // 格式化输出
$doc->load('books.xml', LIBXML_NOBLANKS);

echo "文档编码: " . $doc->encoding . PHP_EOL;
echo "XML 版本: " . $doc->xmlVersion . PHP_EOL;
echo "是否独立: " . ($doc->xmlStandalone ? 'yes' : 'no') . PHP_EOL;

创建新文档

php
<?php
declare(strict_types=1);

$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true;

// 创建根元素
$root = $doc->createElement('configuration');
$doc->appendChild($root);

// 创建子元素
$database = $doc->createElement('database');
$root->appendChild($database);

$host = $doc->createElement('host', 'localhost');
$database->appendChild($host);

$port = $doc->createElement('port', '3306');
$database->appendChild($port);

$name = $doc->createElement('name', 'myapp');
$database->appendChild($name);

// 添加注释
$comment = $doc->createComment('数据库配置');
$root->insertBefore($comment, $database);

// 添加 CDATA
$cdata = $doc->createCDATASection('<strong>重要数据</strong>');
$note = $doc->createElement('note');
$note->appendChild($cdata);
$root->appendChild($note);

echo $doc->saveXML();

查询元素

getElementById

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->loadXML($xmlString);
$doc->validate(); // 需要 DTD 验证 ID 属性

$element = $doc->getElementById('book1');
if ($element) {
    echo $element->nodeValue;
}

getElementsByTagName

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->loadXML($xmlString);

$books = $doc->getElementsByTagName('book');

foreach ($books as $book) {
    $title = $book->getElementsByTagName('title')->item(0);
    $category = $book->getAttribute('category');
    echo "{$category}: {$title->nodeValue}" . PHP_EOL;
}

getElementsByTagNameNS

php
<?php
declare(strict_types=1);

// 带命名空间的 XML
$nsXml = <<<XML
<?xml version="1.0"?>
<root xmlns:ns1="http://example.com/ns1" xmlns:ns2="http://example.com/ns2">
    <ns1:item id="1">命名空间元素1</ns1:item>
    <ns2:item id="2">命名空间元素2</ns2:item>
</root>
XML;

$doc = new DOMDocument();
$doc->loadXML($nsXml);

// 获取特定命名空间的元素
$ns1Items = $doc->getElementsByTagNameNS('http://example.com/ns1', 'item');
foreach ($ns1Items as $item) {
    echo $item->nodeValue . PHP_EOL; // 命名空间元素1
}

XPath 查询

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->loadXML($xmlString);

$xpath = new DOMXPath($doc);

// 基本查询
$titles = $xpath->query('/bookstore/book/title');
foreach ($titles as $title) {
    echo $title->nodeValue . PHP_EOL;
}

// 条件查询
$expensiveBooks = $xpath->query('/bookstore/book[price > 30]/title');
foreach ($expensiveBooks as $title) {
    echo "高价书: {$title->nodeValue}" . PHP_EOL;
}

// 属性查询
$fictionBooks = $xpath->query('/bookstore/book[@category="fiction"]');
foreach ($fictionBooks as $book) {
    $title = $xpath->query('title', $book)->item(0);
    echo "小说: {$title->nodeValue}" . PHP_EOL;
}

// 统计
$count = $xpath->evaluate('count(/bookstore/book)');
echo "书籍总数: {$count}" . PHP_EOL;

// 包含文本匹配
$books = $xpath->query('/bookstore/book[contains(title, "Code")]');

XPath 命名空间

php
<?php
declare(strict_types=1);

$xpath = new DOMXPath($doc);
$xpath->registerNamespace('ns1', 'http://example.com/ns1');
$xpath->registerNamespace('ns2', 'http://example.com/ns2');

// 查询特定命名空间的元素
$items = $xpath->query('//ns1:item');

修改文档

添加和删除节点

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->loadXML($xmlString);
$doc->formatOutput = true;

// 添加新节点
$newBook = $doc->createElement('book');
$newBook->setAttribute('category', 'science');

$title = $doc->createElement('title', 'A Brief History of Time');
$author = $doc->createElement('author', 'Stephen Hawking');
$year = $doc->createElement('year', '1988');
$price = $doc->createElement('price', '25.00');

$newBook->appendChild($title);
$newBook->appendChild($author);
$newBook->appendChild($year);
$newBook->appendChild($price);

// 添加到根元素
$root = $doc->documentElement;
$root->appendChild($newBook);

// 插入到特定位置
$firstBook = $doc->getElementsByTagName('book')->item(0);
$root->insertBefore($newBook, $firstBook);

// 删除节点
$lastBook = $doc->getElementsByTagName('book')->item(
    $doc->getElementsByTagName('book')->length - 1
);
$lastBook->parentNode->removeChild($lastBook);

// 替换节点
$oldPrice = $xpath->query('/bookstore/book[1]/price')->item(0);
$newPrice = $doc->createElement('price', '19.99');
$oldPrice->parentNode->replaceChild($newPrice, $oldPrice);

// 克隆节点
$clonedBook = $newBook->cloneNode(true); // true = 深度克隆
$root->appendChild($clonedBook);

echo $doc->saveXML();

修改属性和文本

php
<?php
declare(strict_types=1);

$doc = new DOMDocument();
$doc->loadXML($xmlString);

// 修改属性
$book = $doc->getElementsByTagName('book')->item(0);
$book->setAttribute('category', 'fantasy');

// 获取和修改属性
$category = $book->getAttribute('category');
$book->removeAttribute('category');

// 判断属性是否存在
if ($book->hasAttribute('category')) {
    echo $book->getAttribute('category');
}

// 修改文本内容
$title = $doc->getElementsByTagName('title')->item(0);
$title->nodeValue = '新标题';

// 修改带命名空间的属性
$book->setAttributeNS(
    'http://www.w3.org/2000/xmlns/',
    'xmlns:custom',
    'http://example.com/custom'
);

echo $doc->saveXML();

遍历 DOM 树

基本遍历

php
<?php
declare(strict_types=1);

function traverseDom(DOMNode $node, int $depth = 0): void
{
    $indent = str_repeat('  ', $depth);

    switch ($node->nodeType) {
        case XML_ELEMENT_NODE:
            $attrs = '';
            if ($node->hasAttributes()) {
                foreach ($node->attributes as $attr) {
                    $attrs .= " {$attr->nodeName}=\"{$attr->nodeValue}\"";
                }
            }
            echo "{$indent}<{$node->nodeName}{$attrs}>" . PHP_EOL;
            break;
        case XML_TEXT_NODE:
            $text = trim($node->nodeValue);
            if ($text !== '') {
                echo "{$indent}Text: {$text}" . PHP_EOL;
            }
            break;
        case XML_COMMENT_NODE:
            echo "{$indent}Comment: {$node->nodeValue}" . PHP_EOL;
            break;
    }

    if ($node->hasChildNodes()) {
        foreach ($node->childNodes as $child) {
            traverseDom($child, $depth + 1);
        }
    }
}

$doc = new DOMDocument();
$doc->loadXML($xmlString);
traverseDom($doc->documentElement);

PHP 8.4 DOM 命名空间新 API

php
<?php
declare(strict_types=1);

// PHP 8.4+ 使用新的 DOM\ 命名空间
use DOM\HTMLDocument;
use DOM\XMLDocument;
use DOM\Element;
use DOM\NodeList;

$doc = XMLDocument::createFromString($xmlString);

// 改进的 API
$books = $doc->getElementsByTagName('book');

// 遍历改进
foreach ($books as $book) {
    // $book 类型为 Element(而不是 DOMElement)
    echo $book->getAttribute('category') . PHP_EOL;
}

// 新增方法
$children = $doc->getElementById('book1');
// ...

HTML 处理

加载 HTML

php
<?php
declare(strict_types=1);

$htmlString = <<<HTML
<!DOCTYPE html>
<html>
<head><title>测试页面</title></head>
<body>
    <div class="container">
        <h1>标题</h1>
        <p class="content">段落内容</p>
        <a href="https://example.com">链接</a>
        <ul>
            <li>项目 1</li>
            <li>项目 2</li>
            <li>项目 3</li>
        </ul>
    </div>
</body>
</html>
HTML;

$doc = new DOMDocument();
// 抑制 HTML 解析时的警告(不严格的 HTML 不会报错)
@$doc->loadHTML($htmlString, LIBXML_NOERROR | LIBXML_NOWARNING);

// 查询元素
$xpath = new DOMXPath($doc);

// 获取所有链接
$links = $xpath->query('//a');
foreach ($links as $link) {
    echo $link->getAttribute('href') . ": " . trim($link->textContent) . PHP_EOL;
}

// 获取列表项
$items = $xpath->query('//ul/li');
foreach ($items as $item) {
    echo "- " . trim($item->textContent) . PHP_EOL;
}

HTML 输出

php
<?php
declare(strict_types=1);

$doc = new DOMDocument('1.0', 'UTF-8');
$doc->loadHTML($htmlString);

// 保存为 HTML
$html = $doc->saveHTML();

// 保存特定节点
$body = $doc->getElementsByTagName('body')->item(0);
$bodyHtml = $doc->saveHTML($body);

// 保存为 XML(XHTML)
$xhtml = $doc->saveXML($doc->documentElement);

实战示例

XML 配置读写

php
<?php
declare(strict_types=1);

/**
 * XML 配置管理器
 */
class XmlConfigManager
{
    public function __construct(
        private readonly string $filePath
    ) {}

    public function get(string $key, mixed $default = null): mixed
    {
        $doc = new DOMDocument();
        $doc->load($this->filePath);

        $xpath = new DOMXPath($doc);
        $nodes = $xpath->query("//setting[@name='{$key}']/value");

        if ($nodes->length > 0) {
            return $nodes->item(0)->nodeValue;
        }

        return $default;
    }

    public function set(string $key, string $value): void
    {
        $doc = new DOMDocument();
        $doc->formatOutput = true;
        $doc->load($this->filePath);

        $xpath = new DOMXPath($doc);
        $existing = $xpath->query("//setting[@name='{$key}']");

        if ($existing->length > 0) {
            // 更新
            $valueNode = $xpath->query("//setting[@name='{$key}']/value")->item(0);
            $valueNode->nodeValue = $value;
        } else {
            // 新增
            $root = $doc->documentElement;
            $setting = $doc->createElement('setting');
            $setting->setAttribute('name', $key);
            $valueNode = $doc->createElement('value', $value);
            $setting->appendChild($valueNode);
            $root->appendChild($setting);
        }

        $doc->save($this->filePath);
    }

    public function getAll(): array
    {
        $doc = new DOMDocument();
        $doc->load($this->filePath);

        $xpath = new DOMXPath($doc);
        $settings = $xpath->query('//setting');
        $result = [];

        foreach ($settings as $setting) {
            $key = $setting->getAttribute('name');
            $value = $xpath->query('value', $setting)->item(0)->nodeValue;
            $result[$key] = $value;
        }

        return $result;
    }

    public function remove(string $key): void
    {
        $doc = new DOMDocument();
        $doc->load($this->filePath);

        $xpath = new DOMXPath($doc);
        $setting = $xpath->query("//setting[@name='{$key}']")->item(0);

        if ($setting) {
            $setting->parentNode->removeChild($setting);
            $doc->save($this->filePath);
        }
    }
}

注意事项

LIBXML 选项

php
<?php
declare(strict_types=1);

// 常用选项
LIBXML_NOBLANKS      // 去除空白文本节点
LIBXML_NOENT          // 替换实体引用
LIBXML_NOERROR        // 抑制错误报告
LIBXML_NOWARNING      // 抑制警告报告
LIBXML_NONET           // 禁止网络访问
LIBXML_NOCDATA         // 合并 CDATA 为文本节点
LIBXML_COMPACT         // 小节点分配优化
LIBXML_PARSE_HUGE      // 放松解析器限制

// 组合使用
$doc->loadXML($xml, LIBXML_NOBLANKS | LIBXML_NOERROR);

最佳实践

  1. 使用 XPath:对于复杂查询,XPath 比遍历更高效
  2. 设置 formatOutput:调试时启用格式化输出
  3. 使用 LIBXML_NOBLANKS:避免空白节点干扰遍历
  4. PHP 8.4+ 使用新 APIDOM\XMLDocument 提供更好的类型安全
  5. 大文件用 XMLReader:对于超大 XML 文件,使用 XMLReader 而非 DOM
  6. XSS 防护:处理 HTML 时使用 htmlspecialchars 转义用户输入

下一节

继续学习:SimpleXML

参考链接