PHP registerNodeClass 和变量名重用

6

当使用registerNodeClass注册新的基本节点类型时:如果我重复使用已创建元素的变量名称,则自定义属性会恢复其默认值。我实际上正在尝试在循环中执行此操作,但这里有一个示例,我认为可以清楚地说明我的意思:

<?php

class myDOMElement extends DOMElement
{
    public $myProp = 'Some default';
}

$doc = new DOMDocument();
$doc->registerNodeClass('DOMElement', 'myDOMElement');

$node = $doc->createElement('a');
$node->myProp = 'A';
$doc->appendChild($node);

# This seems to alter node A in $doc, not what I expected:
$node = $doc->createElement('b');
$node->myProp = 'B';
$doc->appendChild($node);

# Note: $nodeC instead of $node, this works fine. 
$nodeC = $doc->createElement('c');
$nodeC->myProp = 'C';
$doc->appendChild($nodeC);

foreach ($doc->childNodes as $n) {
    echo 'Tag ', $n->tagName, ' myProp:', PHP_EOL;
    var_dump($n->myProp);
}

为什么在标签a中,我得到的是"Some default"而不是值"A"

Tag a myProp:
string(12) "Some default"
Tag b myProp:
string(1) "B"
Tag c myProp:
string(1) "C"

这个问题可能与以下相关:http://stackoverflow.com/q/5473967 - Jackson Pauls
1个回答

2
假设我们使用的是PHP7(至少在PHP版本5..7中,所描述的行为是特有的)。 DOMNode::appendChild方法设置了新的DOMNode对象的内部结构,更新了父节点的内部结构(在我们的情况下是一个DOMDocument对象),然后创建并返回一个基于准备好的内部结构的新的DOMNode对象。实际上,返回的对象和附加的子节点对象是相同的:
$ret_node = $doc->appendChild($node);
debug_zval_dump($node);
debug_zval_dump($ret_node);
var_dump(spl_object_hash($node));
var_dump(spl_object_hash($ret_node));

输出:
object(myDOMElement)#2 (18) refcount(3){
..
object(myDOMElement)#2 (18) refcount(3){
...
string(32) "00000000121277ac00000000658254f1"
string(32) "00000000121277ac00000000658254f1"

DOMNode::$childNodes属性读取处理程序会创建DOMNodeList迭代器对象。当前的迭代器值从准备好的zval中获取,该zval是由php_dom_iterator_move_forward准备的。后者仅基于内部XML结构"创建新对象"(特别是DOMNode)。

但是,php_dom_create_object创建对象的方式很棘手!如果对象是第一次构造的,则通过php_libxml_increment_node_ptr保存指针:

php_libxml_increment_node_ptr((php_libxml_node_object *)intern, obj, (void *)intern);

下次调用 php_dom_create_object 时,它会 检测到已保存的指针,增加引用计数,并返回先前创建的对象
if ((intern = (dom_object *) php_dom_object_get_data((void *) obj))) {
  GC_REFCOUNT(&intern->std)++;
  ZVAL_OBJ(return_value, &intern->std);
  return 1;
}

在释放对象处理程序(当对象被销毁时调用)中,DOM扩展调用 php_libxml_decrement_node_ptr
正如我们所看到的,只要PHP变量存在,DOM对象就会一直存在。如果变量超出了作用域,它将被销毁。在这种情况下,DOM扩展将为我们生成一个新的对象。 现在让我们给myDOMElement类添加一个析构函数:
class myDOMElement extends DOMElement
{
    public $myProp = 'Some default';

    public function __destruct() {
      echo __METHOD__, PHP_EOL;
    }
}

接下来的代码将显示,在我们将$doc->createElement('b')赋值给它的那一行,DOMNode对象正在被销毁:
$node = $doc->createElement('a');
$node->myProp = 'A';
$doc->appendChild($node);

echo "Marker B-1\n";
$node = $doc->createElement('b');
echo "Marker B-2\n";
$node->myProp = 'B';
$doc->appendChild($node);

输出:
Marker B-1
myDOMElement::__destruct
Marker B-2

由于DOM扩展本身不存储zval对象,因此存储在$node变量中的先前对象会自动超出作用域并销毁。从现在开始,我们没有对PHP对象的引用。它的myProp属性也被销毁了。但是,如果我们在循环中请求a节点,则DOM扩展将为其生成新实例:
foreach ($doc->childNodes as $n) {
  var_dump($n->tagName);
}

因此,你问题的答案是:

为什么标签 a 的默认值是 "Some default" 而不是 "A"?

。这个问题的原因在于,当你将另一个对象赋值给 $node 变量时,具有$myProp = "A"的对象实际上被销毁了,因为它超出了作用域,而 DOM 扩展程序不会为我们存储 PHP 对象 - 它将这个责任委托给用户。但是,该节点仍然存在于内部 DOM 结构中。因此,在循环中涉及到 A 标签时,DOM 扩展程序会生成具有默认属性的新对象。

下面是一种解决方法:

foreach (['a', 'b'] as $name) {
  $nodes[] = $node = $doc->createElement($name);
  $node->myProp = $name;
  $doc->appendChild($node);
}
foreach ($doc->childNodes as $n) {
  echo 'Tag ', $n->tagName, ' myProp:'; var_dump($n->myProp);
}
unset($nodes);

Output

Tag a myProp:string(1) "a"
Tag b myProp:string(1) "b"

非常棒的回答,非常感谢您提供了如此详细的信息。我已经为这个问题创建了一个 PHP bug:https://bugs.php.net/bug.php?id=71872 - Jackson Pauls

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