Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructor not called on extended PHP DOMElement

When extending DOMElement in PHP, the constructor of the child class is not called. Nothing jumped out at me in the docs as far as that being expected behavior, but maybe I'm missing something. Here's a simple test case....

 class SillyTestClass extends DOMElement{
    public $foo=null;
    public function __construct($name,$value=null,$namespace=null){
        echo "calling custom construct....";
        $this->foo="bar";
        parent::__construct($name,$value,$namespace);
    }
    public function sayHello(){
        echo "Why, hello there!";
    }
}

$doc=new DOMDocument();
$doc->registerNodeClass('DOMElement','SillyTestClass');
$doc->loadHTML("<div><h1>Sample</h1></div>");
//THIS WORKS! CUSTOM CLASS BEING USED
$doc->documentElement->firstChild->sayHello();

//THIS IS STILL NULL:( Never set by construct, no message saying construct was called either  
echo $doc->documentElement->firstChild->foo; 

Of course if I instantiate it myself it's fine...

$elm=new SillyTestClass("foo","Hi there");
//WORKS! Outputs "bar";
echo $elm->foo;

Why when I register the node class with the DOMDocument does it not call the __construct even though it gives me proper inheritance in every other way?

UPDATE For really curious people or people who know C

======================================================================= Investigation....

This is the DOM extension source taken from PHP src on github

If you were to create an element, this is the chain of events that happens::

document.c :: dom_document_create_element
    |  //uses libxml to generate a new DOMNode
    | node = xmlNewDocNode(docp, NULL, (xmlChar *) name, (xmlChar *) value);

    // that node is then sent to 
  php_dom.c :: php_dom_create_object
   |
   |  //the node type is used to figure out what extension class to use
   |     switch (obj->type) {...
   |    
   |     //that class is used to instance an object
   |     if (domobj && domobj->document) {
   |          ce = dom_get_doc_classmap(domobj->document, ce);
   |     }
        object_init_ex(return_value, ce);

It appears that you do NOT get true inheritance from extending DOMNode or it's built in extension classes (DOMElement, DOMText) if the DOMDocument instances them. In that case, the libxml node is created first and our class properties tacked on second.

This is unfortunate and impossible to get around it seems, because even when you importNode into a document, it instances a new node. Example

class extendsDE extends DOMElement{
   public $constructWasCalled=false;
    public function __construct($name){
       parent::__construct($name);
       $this->constructWasCalled=true;
   }
}

 class extendsDD extends DOMDocument{

    public function __construct(){
        parent::__construct();
        $this->registerNodeClass("DOMElement","extendsDE");
    }
    //@override
    public function createElement($name){
        $elm=new extendsDE($name);
        echo "Element construct called when we create=";
        echo $elm->constructWasCalled?"true":"false";
        return $this->importNode($elm);
   }
}

$doc=new extendsDD();
$node=$doc->createElement("div");
echo "<br/>";
echo "But then when we import into document, a new element is created  and construct called= ";
echo $node->constructWasCalled?"true":"false"; 

Now the debate- is this what the developers intended and the documentation is misleading, or is it a bug and true inheritance was supposed to take place?

like image 862
user2782001 Avatar asked Nov 09 '22 03:11

user2782001


1 Answers

I have figured a way around this in SOME circumstances so far. DOMNodes are kept in memory, so as long as you can get it into the document in one piece (your constructor called), then it will be okay no matter what you do with it or how you access it after that...

Here is an example where we can get the construct to call and still be okay with a document

class extendsDE extends DOMElement{
    public $constructWasCalled=false;
    public function __construct($name){
         parent::__construct($name);
          $this->constructWasCalled=true;
   }
  }

   $doc=new DOMDocument();
   $doc->registerNodeClass("DOMElement","extendsDE");
   $doc->loadHTML("<div></div>");

   //append a node we create manually rather than through createElement
   $node=$doc->getElementsByTagName('div')->item(0)->appendChild(new extendsDE("p"));
    $node->nodeValue="Was my construct called?";
     echo "<br/>";
    echo "A new element was appended and construct called= ";
    echo $node->constructWasCalled?"true":"false";
    echo "<br/>";
    echo "Okay but what happens if I retrieve that node some other way..";
    echo "<br/>";
    echo "what if I get that element through selectors. Custom property still set from constructor=";
   echo $doc->getElementsByTagName('p')->item(0)->constructWasCalled?"true":"false";
  echo "<br/>";
  echo "what if I get that element through relationships. Custom property still set from constructor=";
  echo $doc->getElementsByTagName('div')->item(0)->childNodes->item(0)->constructWasCalled?"true":"false";

THE CATCH:

That only works if you create all the elements. If you load HTML markup using $document->loadHTML($html), your extension constructors won't get called. So far I can only think of hack ways around this like loading the markup and then looping each node to instance replicas and insert them. Definitely possible, but slow...

like image 135
user2782001 Avatar answered Nov 14 '22 23:11

user2782001