在PHP中调试DOMDocument对象

21

我正在尝试调试一个复杂的 PHP DOMDocument 对象。理想情况下,如果我能够让 DOMDocument 以类似数组的格式输出就好了。

DOMDocument:

$dom = new DOMDocument();
$dom->loadHTML("<html><body><p>Hello World</p></body></html>");
var_dump($dom); //或等效的语句

这会输出:

DOMDocument Object ( )

而我希望它输出:

DOMDocument:
html
=>body
==>p
===>Hello World

或者类似这样的格式。为什么没有方便的调试或输出呢?!

6个回答

36
这个回答可能晚了一些,但我很喜欢你的问题!
PHP没有内置直接解决您的问题的方法,因此没有XML dump或类似的东西。
但是,PHP有RecursiveTreeIterator­Docs,它非常接近您的输出:
\-<html>
  \-<body>
    \-<p>
      \-Hello World

(it将会更加美观,如果你的X(HT)ML结构看起来更加复杂。)

它的使用非常简单(就像大多数迭代器一样),可以通过 foreach 进行操作:

$tree = new RecursiveTreeIterator($iterator);
foreach($tree as $key => $value)
{
    echo $value . "\n";
}

你可以将这个内容包装在一个函数中,这样你只需要调用该函数即可。

尽管看起来很简单,但有一个注意点:它需要一个DOMDocument树上的RecursiveIterator。由于PHP无法猜测你需要什么,因此需要将其封装到代码中。正如所写的那样,我发现这个问题很有趣(显然你没有要求XML输出),所以我编写了一些小代码,提供了所需的递归迭代器。下面是代码。

首先,你可能不熟悉PHP中的迭代器。这并不影响你使用我将展示的代码,因为我将会反向使用它,但是,每当你考虑运行自己的一些代码时,请考虑是否可以利用PHP提供的迭代器功能。我写这个是因为它有助于解决常见的问题,并使彼此之间没有真正相关的组件能够协同工作。例如,RecursiveTreeIterator­Docs是内置的,它将与任何你提供的东西一起工作(甚至可以配置它)。但是它需要一个RecursiveIterator才能操作。

因此,让我们给它一个 RecursiveIterator,为 DOMNodes 提供 <tag>(如果它们是标签(元素))并且只提供 text 如果它们是文本节点:

class DOMRecursiveDecoratorStringAsCurrent extends RecursiveIteratorDecoratorStub
{
    public function current()
    {
        $node = parent::current();
        $nodeType = $node->nodeType;

        switch($nodeType)
        {
            case XML_ELEMENT_NODE:
                return "<$node->tagName>";

            case XML_TEXT_NODE:
                return $node->nodeValue;

            default:
                return sprintf('(%d) %s', $nodeType, $node->nodeValue);
        }
    }
}

这个DOMRecursiveDecoratorStringAsCurrent类(名称仅作示例)利用了RecursiveIteratorDecoratorStub中的一些抽象代码。然而,最重要的部分是::current函数,它只返回bracketsWikipedia<>)中一个DOMNodetagName和textnodes的文本,就是输出所需的内容,所以编码只需要这些。

实际上,除非您也拥有抽象代码,否则这个程序无法正常工作,但为了可视化代码如何使用(最有趣的部分),让我们来看一下:

$iterator = new DOMRecursiveDecoratorStringAsCurrent($iterator);
$tree = new RecursiveTreeIterator($iterator);
foreach($tree as $key => $value)
{
    echo $value . "\n";
}

由于是倒序操作,因此我们目前已经根据要由RecursiveTreeIterator显示的DOMNode指定了输出。目前为止还好,很容易理解。但缺失的关键在于抽象代码内部以及如何创建一个DOMElement内所有节点的RecursiveIterator。预览整个代码的调用方式(如前所述,您可以将其放入一个函数中,以便在代码中轻松访问以进行调试。可能会有一个名为xmltree_dump的函数):

$dom = new DOMDocument();
$dom->loadHTML("<html><body><p>Hello World</p></body></html>");
$iterator = new DOMRecursiveIterator($dom->documentElement);
$iterator = new DOMRecursiveDecoratorStringAsCurrent($iterator);
$tree = new RecursiveTreeIterator($iterator);
foreach($tree as $key => $value)
{
    echo $value . "\n";
}

除了已经涵盖的代码之外,我们还有什么?首先是一个DOMRecursiveIterator - 就是这样。其余的代码都是标准的DOMDocument代码。
那么让我们来谈谈DOMRecursiveIterator。它是在RecursiveTreeIterator中所需的RecursiveIterator。它被修饰,使得树的转储实际上打印带括号的标签名和原样文本。
现在可能值得分享一下它的代码:
class DOMRecursiveIterator extends DOMIterator implements RecursiveIterator
{
    public function hasChildren()
    {
        return $this->current()->hasChildNodes();
    }
    public function getChildren()
    {
        $children = $this->current()->childNodes;
        return new self($children);
    }
}

这是一个非常简短的类,只有两个函数。在这里我有点作弊,因为这个类也继承自另一个类。但是按照原文写的话,这是不正确的,所以这个类实际上负责递归:hasChildrengetChildren。显然,即使这两个函数没有太多的代码,它们也只是将“问题”(hasChildrengetChildren?)映射到标准的DOMNode上。如果一个节点有子节点,那么就回答“是”或者返回它们(而且这是一个迭代器,在迭代器的形式下返回它们,因此使用new self())。
因为这很简短,所以在处理完后,可以继续处理父类DOMIteratorimplements RecursiveIterator­Docs只是为了使它能够工作):
class DOMIterator extends IteratorDecoratorStub
{
    public function __construct($nodeOrNodes)
    {
        if ($nodeOrNodes instanceof DOMNode)
        {
            $nodeOrNodes = array($nodeOrNodes);
        }
        elseif ($nodeOrNodes instanceof DOMNodeList)
        {
            $nodeOrNodes = new IteratorIterator($nodeOrNodes);
        }
        if (is_array($nodeOrNodes))
        {
            $nodeOrNodes = new ArrayIterator($nodeOrNodes);
        }

        if (! $nodeOrNodes instanceof Iterator)
        {
            throw new InvalidArgumentException('Not an array, DOMNode or DOMNodeList given.');
        }

        parent::__construct($nodeOrNodes);
    }
}

这是DOMPHP的基本迭代器,它只需要一个DOMNodeDOMNodeList来进行迭代。也许这听起来有点多余,因为DOM已经支持了DOMNodeList,但它不支持RecursiveIterator,而我们已经知道我们需要一个RecursiveTreeIterator来输出。所以在它的构造函数中,创建了一个Iterator并传递给父类,这个父类又是抽象代码。当然,我马上就会揭示这段代码。由于这是反向的,让我们回顾一下到目前为止做了什么:
  • 使用RecursiveTreeIterator实现树形输出。
  • 使用DOMRecursiveDecoratorStringAsCurrentDOMNode可视化为树形结构。
  • 使用DOMRecursiveIteratorDOMIterator递归迭代DOMDocument中的所有节点。

这些是定义方面所需的内容,但我称之为抽象代码仍然缺失。它只是一种简单的代理代码,将相同的方法委托给另一个对象。相关模式被称为装饰器。然而,这只是代码,首先是Iterator,然后是它的RecursiveIterator朋友:

abstract class IteratorDecoratorStub implements OuterIterator
{
    private $iterator;
    public function __construct(Iterator $iterator)
    {
        $this->iterator = $iterator;
    }
    public function getInnerIterator()
    {
        return $this->iterator;
    }
    public function rewind()
    {
        $this->iterator->rewind();
    }
    public function valid()
    {
        return $this->iterator->valid();
    }
    public function current()
    {
        return $this->iterator->current();
    }
    public function key()
    {
        return $this->iterator->key();
    }
    public function next()
    {
        $this->iterator->next(); 
    }
}

abstract class RecursiveIteratorDecoratorStub extends IteratorDecoratorStub implements RecursiveIterator
{
    public function __construct(RecursiveIterator $iterator)
    {
        parent::__construct($iterator);
    }
    public function hasChildren()
    {
        return $this->getInnerIterator()->hasChildren();
    }
public function getChildren()
{
    return new static($this->getInnerIterator()->getChildren());
}
}

这并不是什么神奇的事情,只是将方法调用委托给其继承的对象$iterator。看起来像是重复的,而好的迭代器就是关于重复的。我将它们放入抽象类中,这样我只需要编写这个非常简单的代码一次。所以至少我自己不需要重复自己。

其他已经讨论过的类使用了这两个抽象类。因为它们非常简单,所以我一直拖到这里才讲。

好了,读到这里可能有点多,但好消息是,就只有这些。

简而言之:PHP没有内置此功能,但您可以自己编写相当简单且可重用的代码。如前所述,将其封装到一个名为xmltree_dump的函数中是个好主意,以便于调试时轻松调用:

function xmltree_dump(DOMNode $node)
{
    $iterator = new DOMRecursiveIterator($node);
    $decorated = new DOMRecursiveDecoratorStringAsCurrent($iterator);
    $tree = new RecursiveTreeIterator($decorated);
    foreach($tree as $key => $value)
    {
        echo $value . "\n";
    }
}

使用方法:

$dom = new DOMDocument();
$dom->loadHTML("<html><body><p>Hello World</p></body></html>");
xmltree_dump($dom->documentElement);

需要的唯一一件事情就是包含/引用所有使用的类定义。你可以将它们放在一个文件中并使用require_once或与您可能正在使用的自动加载器集成。完整代码一次性
如果需要编辑输出方式,可以编辑DOMRecursiveDecoratorStringAsCurrent或更改xmltree_dump内部的RecursiveTreeIterator配置。希望这对您有所帮助(即使相当冗长,反着说也是相当间接的)。

17
+1 ... 而且你还在圣诞节写了这些。解锁成就“永远单身”。 - Dunhamzzz
我遇到了Catchable fatal error: Argument 1 passed to IteratorIterator::__construct() must implement interface Traversable, instance of DOMNodeList given的错误,我做错了什么?我从gist中获取了代码,并在底部的usage块中使用了最终示例... - cwd
当然,它应该可以工作,在哪一行你遇到了错误?是在gist line 67上的$nodeOrNodes = new IteratorIterator($nodeOrNodes);吗? - hakre
这非常有用!它帮助了我很多。谢谢 :) - marlar
@hakre,你是最棒的! - Tim Groeneveld

15

1
如果想要调试一个不是文档本身而是其中一部分的DOM元素:$element->ownerDocument->saveXML($element)); - tanius

10

对于一个DOM节点,只需要使用以下代码:

print_r(simplexml_import_dom($entry)->asXML());

0

虽然我自己没有尝试过,但可以看看Zend_Dom,它是Zend Framework的一部分。大多数Zend Framework组件的文档和示例都非常详细。


-1
我刚刚使用了DOMDocument :: save。它必须写入文件,这有点糟糕,但无论如何。

2
如果你这样做了,你可以直接使用saveHTML并将其保存到字符串中。 - Paolo Bergantino

-1

你可以作弊并使用JSON将其转换为数组来检查结构。

print_r(json_decode(json_encode($node), true));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接