How to move inner content with DOMDocumentFragment? - php

I have a horrible algorithm, to "remove a node", moving its inner content to its parent node (see below)... But I think is possible to develop a better algorithm, using DOMDocumentFragment (and not using saveXML/loadXML).
The algorithm below was inspired by renameNode().
/**
* Move the content of the $from node to its parent node.
* Conditions: parent not a document root, $from not a text node.
* #param DOMElement $from to be removed, preserving its contents.
* #return true if changed, false if not.
*/
function moveInner($from) {
$to = $from->parentNode;
if ($from->nodeType==1 && $to->parentNode->nodeType==1) {
// Scans $from, and record information:
$lst = array(); // to avoid "scan bugs" of DomNodeList iterator
foreach ($to->childNodes as $e)
$lst[] = array($e);
for($i=0; $i<count($lst); $i++)
if ($lst[$i][0]->nodeType==1 && $from->isSameNode($lst[$i][0])) {
$lst[$i][1] = array();
foreach ($lst[$i][0]->childNodes as $e)
$lst[$i][1][] = $e;
}
// Build $newTo (rebuilds the parent node):
$newTo = $from->ownerDocument->createElement($to->nodeName);
foreach ($to->attributes as $a) {
$newTo->setAttribute($a->nodeName, $a->nodeValue);
}
foreach ($lst as $r) {
if (count($r)==1)
$newTo->appendChild($r[0]);
else foreach ($r[1] as $e)
$newTo->appendChild($e);
}
// Replaces it:
$to->parentNode->replaceChild($newTo, $to);
return true;
} else
return false;
}
Example
INPUT
<html id="root">
<p id="p1"><i>Title</i></p>
<p id="p2"><b id="b1">Rosangela<sup>1</sup>, Maria<sup>2</sup></b>,
<b>Eduardo<sup>4</sup></b>
</p>
</html>
OUTPUT of moveInner($dom->getElementById('p1'))
... <p id="p1">Title</p> ...
OUTPUT of moveInner($dom->getElementById('b1'))
... <p id="p2">Rosangela<sup>1</sup>, Maria<sup>2</sup>,
<b>Eduardo<sup>4</sup></b>
</p> ...
There are no changes in moveInner($dom->getElementById('root')), or moveInner($dom->getElementById('p1')) after first use.
PS: is like a "TRIM TAG" function.

As you move inside the same document this actually is by far not so much hassle. The code you've posted alone had already many places that could be optimized just on it's own, for example to turn a childNodes NodeList into an array just use iterator_to_array:
$children = iterator_to_array($from->childNodes);
Also you should use more speaking variable names, there is no problem in having longer names. It just makes the code more readable and leaves the room to view the more important stuff faster:
/**
* Move the content of the $from node to its parent node.
*
* #param DOMElement $from to be removed, preserving its contents.
* #return DOMElement the element removed (w/o it's former children)
* #throws InvalidArgumentException in case there is no parent element
*/
function moveInner(DOMElement $from)
{
if (!$from->parentNode instanceof DOMElement) {
throw new InvalidArgumentException(
'DOMElement does not have a parent DOMElement node.'
);
}
/** #var DOMNode[] $children */
$children = iterator_to_array($from->childNodes);
foreach ($children as $child) {
$from->parentNode->insertBefore($child, $from);
}
return $from->parentNode->removeChild($from);
}
It just works. If you insert the same element into another place in DOMDocument, the element is moved, not duplicated.
If you want to duplicate (so to preserve the child, not move it), you can use the child nodes as prototypes and just clone them. As this function returns the element that is removed, it the contains the copy.
First the example w/o clone, just the function as above:
$removed = moveInner($doc->getElementById('b1'));
echo $doc->saveHTML(), "\nRemoved: ", $doc->saveHTML($removed);
Output:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html id="root"><body><p id="p1"><i>Title <b>2</b></i></p>
<p id="p2">Rosangela<sup>1</sup>, Maria<sup>2</sup>,
<b>Eduardo<sup>4</sup></b>
</p>
</body></html>
Removed: <b id="b1"></b>
Then second the modified function, the change is just adding clone in the following line:
$from->parentNode->insertBefore(clone $child, $from);
#####
Output:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html id="root"><body><p id="p1"><i>Title <b>2</b></i></p>
<p id="p2">Rosangela<sup>1</sup>, Maria<sup>2</sup>,
<b>Eduardo<sup>4</sup></b>
</p>
</body></html>
Removed: <b id="b1">Rosangela<sup>1</sup>, Maria<sup>2</sup></b>
I hope this is helpful and matches your needs. You really had very much code there, probably a bit misleaded by the replace node scenario which is different. Also in that scenario I was patching some different error code that is not always the best base to change into good code.
This btw. reminded me about a question where clone was also very helpful which I just answered today:
Appending an Element multiple times in DOMDocument

You can try to use phpQuery for this task. It has syntax similar to the jquery.
Some thing like this pq("#b1")->html(pq("#b1")->text());
link to phpQuery

Related

PHP & XML Creation: Node in wrong parent

I am working currently on an OOP based PHP project in MVC style.
For my project i need to create/send/recieve/process XMLs.
Now i have a BIG problem with creating XML-Structures with DOMDocument.
Everytime i create a new XML-Node without an attributes or values, all nodes afterwards will be a child if this node!
In other words: I can not create an empty XML-Node without all nodes afterwards beeing a child of this empty node!!!
This problem bugs me for while now but I really need the way I am dealing right now with the XML creation.
I couldn't find any solutions but some similar problems.
This PHP tests my XmlHandler-Class, which creates the XML-Request:
Test.php:
<?php
include "Handler/XmlHandler.php";
$xmlHandler=new XmlHandler();
$xmlHandler->CreateNewXmlInstance();
$root = $xmlHandler->CreateRootNode('RootNode');
$l1 = $xmlHandler->AppendNodeWithChild($root, "NodeLevel1", "Text1 - This node one has text");
$l2 = $xmlHandler->AppendNodeWithChild($root, "NodeLevel2", "Text2 - Next node Level3 level is not gonna have text");
$l21 = $xmlHandler->AppendNodeWithChild($l2, "NodeLevel2_1", "Text2_1 - This node will be a child of Level2, everything fine");
$l3 = $xmlHandler->AppendNodeWithChild($root, "NodeLevel3", "");
$l4 = $xmlHandler->AppendNodeWithChild($root, "NodeLevel4", "Text4 - This node should be on same level like 3, 2 & 1, but instead it's a child of Level 3 (?!?!?!?!)");
echo "<p style='display:none;'>".$xmlHandler->SaveXml()."</p>";
?>
Here is the XML-Handler Class which i use to create the XML-Request-Structure (i just posted the neccessary parts of the class here)
XmlHandler.php:
<?php
class XmlHandler{
private $xml;
/**
*
* Constructor
*
*/
function __construct()
{
$this->xml=null;
}
//[...]
/*
* Custom XML-Creator Functions
*
*/
public function CreateNewXmlInstance(){
/*********************************************/
/** XML DOM example of building XML Request **/
/*********************************************/
$this->xml = new DOMDocument('1.0', 'UTF-8');
return $this->xml;
}
public function CreateRootNode($name){
$rootElement = $this->xml->appendChild( $this->xml->createElement($name) );
return $rootElement;
}
public function AppendNodeWithChild($node, $childName, $childText){
$result = $node->appendChild($this->xml->createElement($childName));
if(null != $childText && !empty($childText)){
$result->appendChild( $this->xml->createTextNode($childText) );
}
return $result;
}
public function SetNodeAttributes($node, $nameAndValues){
if(null != $nameAndValues && sizeof($nameAndValues) > 0){
foreach($nameAndValues as $name => $value){
$this->SetNodeAttribute($node, $name, $value);
}
}
}
public function SetNodeAttribute($node, $name, $value){
$node->setAttribute($name, $value);
}
public function SaveXml(){
return $this->xml->saveXML();
}
//[...]
}
?>
This is the Result:
<!-- ?xml version="1.0" encoding="UTF-8"? -->
<rootnode>
<nodelevel1>Text1 - This node one has text</nodelevel1>
<nodelevel2>Text2 - Next node Level3 level is not gonna have text
<nodelevel2_1>Text2_1 - This node will be a child of Level2, everything fine</nodelevel2_1>
</nodelevel2>
<nodelevel3>
<nodelevel4>Text4 - This node should be on same level like 3, 2 & 1, but instead it's a child of Level 3 (?!?!?!?!)</nodelevel4>
</nodelevel3>
</rootnode>
But in theory, there should be somethign like that:
<!-- ?xml version="1.0" encoding="UTF-8"? -->
<rootnode>
<nodelevel1>Text1 - This node one has text</nodelevel1>
<nodelevel2>Text2 - Next node Level3 level is not gonna have text
<nodelevel2_1>Text2_1 - This node will be a child of Level2, everything fine</nodelevel2_1>
</nodelevel2>
<nodelevel3/>
<nodelevel4>Text4 - This node should be on same level like 3, 2 & 1, but instead it's a child of Level 3 (?!?!?!?!)</nodelevel4>
</rootnode>
As you can see: Something went wrong when i had not set a value for the new created Node on Level3!:
$l3 = $xmlHandler->AppendNodeWithChild($root, "NodeLevel3", "");
$l4 = $xmlHandler->AppendNodeWithChild($root, "NodeLevel4", "Text4 - This node should be on same level like 3, 2 & 1, but instead it's a child of Level 3 (?!?!?!?!)");
As long as i set attributes or put in value sin the new created node, everything is fine.
But i have some situations where also pure empty nodes have to be created!
My question is:
What am I doing wrong here?
Or does PHP do something wrong?
Maybe my browser does a bad preparation of the XML, but the outgoing XML request was build and send correctly and the mistake lies something else?
If so, how can I check the XML request though?
Edit Nr.2:
I changed my question/original post somehow.
The above example is a bit more easy to understand.
At least i hope so.
Wrap your helper functions in a class
class XMLHelper {
/*
* Custom XML-Creator Functions
*
*/
private $xml;
public function CreateNewXmlInstance(){
/*********************************************/
/** XML DOM example of building XML Request **/
/*********************************************/
$this->xml = new DOMDocument('1.0', 'UTF-8');
return $this->xml;
}
public function CreateRootNode($name){
$rootElement = $this->xml->appendChild( $this->xml->createElement($name) );
return $rootElement;
}
public function AppendNodeWithChild($node, $childName, $childText){
$result = $node->appendChild($this->xml->createElement($childName));
if(null != $childText && !empty($childText)){
$result->appendChild( $this->xml->createTextNode($childText) );
}
return $result;
}
public function SetNodeAttributes($node, $nameAndValues){
if(null != $nameAndValues && sizeof($nameAndValues) > 0){
foreach($nameAndValues as $name => $value){
$this->SetNodeAttribute($node, $name, $value);
}
}
}
public function SetNodeAttribute($node, $name, $value){
$node->setAttribute($name, $value);
}
public function SaveXml(){
return $this->xml->saveXML();
}
}
Wrap things in class because your code contain $this calls
Wrapping things in class make $this calls to correct variables the program needs.
Then new the class and initialize the nodes
$test = new XMLHelper();
$test->CreateNewXmlInstance();
$request = $test->CreateRootNode("request");
$node1 = $test->AppendNodeWithChild($request, "node1", null);
$node2 = $test->AppendNodeWithChild($node1, "node2", null);
$test->SetNodeAttributes($node2, array(
"client" => "This is a testing value"
));
echo $test->saveXml();
//Output:
// <?xml version="1.0" encoding="UTF-8"?>
// <request>
// <node1>
// <node2 client="This is a testing value"/>
// </node1>
// </request>
Please be noted that code need to executed in a correct sequence.
New the root node ( request )
Then append a node 1 to ( request )
Finally append node 2 to node 1
Configure the client value of node 2
Print out the whole root node
Then you've done the magic
There is nothing wrong with PHP neither with your web browser.
The error probably come from you haven't initialize the root node and not adding the created node correctly to the root node.
For XML validation in php, see this link.
DOMDocument has a function built-in for validation.
Edit 2016-08-10
Here are the revised code of your case
class SpecificXmlHandler extends XmlHandler{
/**
* Constructor
*
*/
private $errorCounter;
private $xmlUrl;
//Declare the root first
private $root;
function __construct()
{
parent::__construct();
$this->errorCounter=0;
$this->xmlUrl=Configuration::XML_REQUEST_URL;
}
//[...]
/**
* Action Functions
*/
public function GetStaticData($requestName, $requestFilterNamesAndValues){
$xml = $this->BuildStaticDataRequest($requestName, $requestFilterNamesAndValues);
echo "<p style='display:none'>" . $xml. "</p>"; //Request
$response = $this->ExecuteRequest($this->xmlUrl, $xml, null, false);
echo "<p style='display:none'>" . $response . "</p>"; //Resonse
//[...]
}
//[...]
/**
* Request Building Functions
*/
public function BuildStaticDataRequest($requestName, $requestFilterNamesAndValues){
$this->CreateNewXmlInstance();
//$root = $this->CreateRootNode('Request');
//Use the root node you have created in the constructor by using $this
$this->root = $this->CreateRootNode('Request');
//Generate Header (Source-Node)
$this->GenerateHeadData($this->root);
//[...]
return $this->SaveXml();
}
public function GenerateHeadData($root){
$clientID=Configuration::XML_CLIENT;
//Here the Node1 & Node2 creation
$node1 = $this->AppendNodeWithChild($root, "node1", null);
$node2 = $this->AppendNodeWithChild($node1, "node2", null);
$this->SetNodeAttributes( $node2 , array(
"Client" =>$clientID
));
//Change it to return the whole root
return $root;
}
[...]
}
Please create variables in the class and use $this to refer the private variables you have created.
Ok guys, somebody helped me with this problem.
Here is the conversation:
Him: The tags are wrong (since they've been changed to all-lowercase)
Me: Yes, it seems the XML gets messy after echoing it out into the browser instead of saving it into a file!
Him: And the output shouldn't even be formatted
Me: I did that myself for a better view
Him: This is the raw output of $xmlHandler->SaveXml() after enabling formatted output
<?xml version="1.0" encoding="UTF-8"?>
<RootNode>
<NodeLevel1>Text1 - This node one has text</NodeLevel1>
<NodeLevel2>Text2 - Next node Level3 level is not gonna have text<NodeLevel2_1>Text2_1 - This node will be a child of Level2, everything fine</NodeLevel2_1></NodeLevel2>
<NodeLevel3/>
<NodeLevel4>Text4 - This node should be on same level like 3, 2 & 1, but instead it's a child of Level 3 (?!?!?!?!)</NodeLevel4>
</RootNode>
Me: After printing it out into a file, i got the same result as you.
The conclusion:
Now I've wrote the saveXml-output into a file.
The Result is he same as Him told me.
So were was the mess?
I thoght it would be enough to print the XML out into the browser with echo and check it through the HTML-sourcecode via FireFox.
As you can see in my first post, i used echo...
echo "<p style='display:none;'>".$xmlHandler->SaveXml()."</p>";
to somehow print out the result of my XML-Creation (because i didn't know how else).
Therefore the XML-Creation was a full success from start up and the error i get from the response-Server lies somewhere else (from now on i can not rely on your help anymore)!

PHP DomDocument, append element of a page to another

I have a php script for generate a shop.
So, first i retrieve my html page with dom document :
$oPage = new webHTML("boutique_panier_HTML");
$oInter = $oPage->getElementById("inter");
webHTML() is just a custom DomDocument class. So, i retrieve my principal div (inter) and i do some treatments into this div before return $oPage->saveHTML();
So, for now, it's ok.
I need to load another page, retrieve an element (form) and put this element on my $oInter.
So, juste before return $oPage->saveHTML();, i do :
$oPage2 = new webHTML("formulaire_bon_commande");
$oInter2 = $oPage2->getElementsByTagName("form");
$oInter->appendChild($oInter2);
So, i load the page "formulaire_bon_commande", i retrieve my element form, and i try to append this element to my $oInter div.
And with this code, i have just a white page... No effect. Any ideas ?
method getElementsByTagName returns DOMNodeList, appendChild expects DOMNode, so you have to iterate $oInter2
$oInter2 = $oPage2->getElementsByTagName("form");
foreach ($oInter2 as $el){
$node = $oPage->importNode($el, true);
$oInter->appendChild($node);
}
Example:
$oPage = new DOMDocument();
$oPage->loadHTML('<html><p id="inter"></p></html>');
$oInter = $oPage->getElementById("inter");
$oPage2 = new DOMDocument();
$oPage2->loadHTML('<html><form><button></button></form></html>');
$oInter2 = $oPage2->getElementsByTagName("form");
foreach($oInter2 as $el) {
$node = $oPage->importNode($el, true);
$oInter->appendChild($node);
}
echo $oPage->saveHTML();
output:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><p id="inter"><form><button></button></form></p></body></html>

How to change root of a node with DOMDocument methods?

How to only change root's tag name of a DOM node?
In the DOM-Document model we can not change the property documentElement of a DOMElement object, so, we need "rebuild" the node... But how to "rebuild" with childNodes property?
NOTE: I can do this by converting to string with saveXML and cuting root by regular expressions... But it is a workaround, not a DOM-solution.
Tried but not works, PHP examples
PHP example (not works, but WHY?):
Try-1
// DOMElement::documentElement can not be changed, so...
function DomElement_renameRoot1($ele,$ROOTAG='newRoot') {
if (gettype($ele)=='object' && $ele->nodeType==XML_ELEMENT_NODE) {
$doc = new DOMDocument();
$eaux = $doc->createElement($ROOTAG); // DOMElement
foreach ($ele->childNodes as $node)
if ($node->nodeType == 1) // DOMElement
$eaux->appendChild($node); // error!
elseif ($node->nodeType == 3) // DOMText
$eaux->appendChild($node); // error!
return $eaux;
} else
die("ERROR: invalid DOM object as input");
}
The appendChild($node) cause an error:
Fatal error: Uncaught exception 'DOMException'
with message 'Wrong Document Error'
Try-2
From #can suggestion (only pointing link) and my interpretation of the poor dom-domdocument-renamenode manual.
function DomElement_renameRoot2($ele,$ROOTAG='newRoot') {
$ele->ownerDocument->renameNode($ele,null,"h1");
return $ele;
}
The renameNode() method caused an error,
Warning: DOMDocument::renameNode(): Not yet implemented
Try-3
From PHP manual, comment 1.
function renameNode(DOMElement $node, $newName)
{
$newNode = $node->ownerDocument->createElement($newName);
foreach ($node->attributes as $attribute)
$newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);
while ($node->firstChild)
$newNode->appendChild($node->firstChild); // changes firstChild to next!?
$node->ownerDocument->replaceChild($newNode, $node); // changes $node?
// not need return $newNode;
}
The replaceChild() method caused an error,
Fatal error: Uncaught exception 'DOMException' with message 'Not Found Error'
As this has not been really answered yet, the error you get about not found is because of a little error in the renameNode() function you've copied.
In a somewhat related question about renaming different elements in the DOM I've seen this problem as well and used an adoption of that function in my answer that does not have this error:
/**
* Renames a node in a DOM Document.
*
* #param DOMElement $node
* #param string $name
*
* #return DOMNode
*/
function dom_rename_element(DOMElement $node, $name) {
$renamed = $node->ownerDocument->createElement($name);
foreach ($node->attributes as $attribute) {
$renamed->setAttribute($attribute->nodeName, $attribute->nodeValue);
}
while ($node->firstChild) {
$renamed->appendChild($node->firstChild);
}
return $node->parentNode->replaceChild($renamed, $node);
}
You might have spotted it in the last line of the function body: This is using ->parentNode instead of ->ownerDocument. As $node was not a child of the document, you did get the error. And it also was wrong to assume that it should be. Instead use the parent element to search for the child in there to replace it ;)
This has not been outlined in the PHP manual usernotes so far, however, if you did follow the link to the blog-post that originally suggested the renameNode() function you could find a comment below it offering this solution as well.
Anyway, my variant here uses a slightly different variable naming and is more distinct about the types. Like the example in the PHP manual it misses the variant that deals with namespace nodes. I'm not yet booked what would be best, e.g. creating an additional function dealing with it, taking over namespace from the node to rename or changing the namespace explicitly in a different function.
First, you need to understand that the DOMDocument is only the hierarchical root of the document-tree. It's name is always #document. You want to rename the root-element, which is the $document->documentElement.
If you want to copy nodes form a document to another document, you'll need to use the importNode() function: $document->importNode($nodeInAnotherDocument)
Edit:
renameNode() is not implemented yet, so you should make another root, and simply replace it with the old one. If you use DOMDocument->createElement() you don't need to use importNode() on it later.
$oldRoot = $doc->documentElement;
$newRoot = $doc->createElement('new-root');
foreach ($oldRoot->attributes as $attr) {
$newRoot->setAttribute($attr->nodeName, $attr->nodeValue);
}
while ($oldRoot->firstChild) {
$newRoot->appendChild($oldRoot->firstChild);
}
$doc->replaceChild($newRoot, $oldRoot);
This is an variation of my "Try-3" (see question), and works fine!
function xml_renameNode(DOMElement $node, $newName, $cpAttr=true) {
$newNode = $node->ownerDocument->createElement($newName);
if ($cpAttr && is_array($cpAttr)) {
foreach ($cpAttr as $k=>$v)
$newNode->setAttribute($k, $v);
} elseif ($cpAttr)
foreach ($node->attributes as $attribute)
$newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);
while ($node->firstChild)
$newNode->appendChild($node->firstChild);
return $newNode;
}    
Of course, if you show how to use DOMDocument::renameNode (without errors!), the bounty goes for you!
ISTM in your approach you attempt to import nodes from another DOMDocument, so you need to use the importNode() method:
$d = new DOMDocument();
/* Make a `foo` element the root element of $d */
$root = $d->createElement("foo");
$d->appendChild($root);
/* Append a `bar` element as the child element of the root of $d */
$child = $d->createElement("bar");
$root->appendChild($child);
/* New document */
$d2 = new DOMDocument();
/* Make a `baz` element the root element of $d2 */
$root2 = $d2->createElement("baz");
$d2->appendChild($root2);
/*
* Import a clone of $child (from $d) into $d2,
* with its child nodes imported recursively
*/
$child2 = $d2->importNode($child, true);
/* Add the clone as the child node of the root of $d2 */
$root2->appendChild($child2);
However, it is far easier to append the child nodes to a new parent element (thereby moving them), and replace the old root with that parent element:
$d = new DOMDocument();
/* Make a `foo` element the root element of $d */
$root = $d->createElement("foo");
$d->appendChild($root);
/* Append a `bar` element as the child element of the root of $d */
$child = $d->createElement("bar");
$root->appendChild($child);
/* <?xml version="1.0"?>
<foo><bar/></foo> */
echo $d->saveXML();
$root2 = $d->createElement("baz");
/* Make the `bar` element the child element of `baz` */
$root2->appendChild($child);
/* Replace `foo` with `baz` */
$d->replaceChild($root2, $root);
/* <?xml version="1.0"?>
<baz><bar/></baz> */
echo $d->saveXML();
I hope I am not missing anything but I happened to have the similar problem and was able to solve it by using use DomDocument::replaceChild(...).
/* #var $doc DOMDocument */
$doc = DOMImplementation::createDocument(NULL, 'oldRoot');
/* #var $newRoot DomElement */
$newRoot = $doc->createElement('newRoot');
/* all the code to create the elements under $newRoot */
$doc->replaceChild($newRoot, $doc->documentElement);
$doc->documentElement->isSameNode($newRoot) === true;
What threw me off initially was that $doc->documentElement was readonly, but the above worked and seems to be much simpler solution IF the $newRoot was created with the same DomDocument, otherwise you'll need do the importNode solution as described above. From your question is appears that $newRoot could be created from the same $doc.
Let us know if this worked out for you. Cheers.
EDIT: Noticed in version 20031129 that the DomDocument::$formatOutput, if set, does not format $newRoot output when you finally call $doc->saveXML()

How to serialize/save a DOMElement in $_SESSION?

I'm pretty new to PHP, DOM, and the PHP DOM implementation. What I'm trying to do is save the root element of the DOMDocument in a $_SESSION variable so I can access it and modify it on subsequent page loads.
But I get an error in PHP when using $_SESSION to save state of DOMElement:
Warning: DOMNode::appendChild() [domnode.appendchild]: Couldn't fetch DOMElement
I have read that a PHP DOMDocument object cannot be saved to $_SESSION natively. However it can be saved by saving the serialization of the DOMDocument (e.g. $_SESSION['dom'] = $dom->saveXML()).
I don't know if the same holds true for saving a DOMElement to a $_SESSION variable as well, but that's what I was trying. My reason for wanting to do this is to use an extended class of DOMElement with one additional property. I was hoping that by saving the root DOMElement in $_SESSION that I could later retrieve the element and modify this additional property and perform a test like, if (additionalProperty === false) { do something; }. I've also read that by saving a DOMDocument, and later retrieving it, all elements are returned as objects from native DOM classes. That is to say, even if I used an extended class to create elements, the property that I subsequently need will not be accessible, because the variable holding reference to the extended-class object has gone out of scope--which is why I'm trying this other thing. I tried using the extended class (not included below) first, but got errors...so I reverted to using a DOMElement object to see if that was the problem, but I'm still getting the same errors. Here's the code:
<?php
session_start();
$rootTag = 'root';
$doc = new DOMDocument;
if (!isset($_SESSION[$rootTag])) {
$_SESSION[$rootTag] = new DOMElement($rootTag);
}
$root = $doc->appendChild($_SESSION[$rootTag]);
//$root = $doc->appendChild($doc->importNode($_SESSION[$rootTag], true));
$child = new DOMElement('child_element');
$n = $root->appendChild($child);
$ct = 0;
foreach ($root->childNodes as $ch) echo '<br/>'.$ch->tagName.' '.++$ct;
$_SESSION[$rootTag] = $doc->documentElement;
?>
This code gives the following errors (depending on whether I use appendChild directly or the commented line of code using importNode):
Warning: DOMNode::appendChild() [domnode.appendchild]: Couldn't fetch DOMElement in C:\Program Files\wamp_server_2.2\www\test2.php on line 11
Warning: DOMDocument::importNode() [domdocument.importnode]: Couldn't fetch DOMElement in C:\Program Files\wamp_server_2.2\www\test2.php on line 12
I have several questions. First, what is causing this error and how do I fix it? Also, if what I'm trying to do isn't possible, then how can I accomplish my general objective of saving the 'state' of a DOM tree while using a custom property for each element? Note that the additional property is only used in the program and is not an attribute to be saved in the XML file. Also, I can't just save the DOM back to file each time, because the DOMDocument, after a modification, may not be valid according to a schema I'm using until later when additional modificaitons/additions have been performed to the DOMDocument. That's why I need to save a temporarily invalid DOMDocument. Thanks for any advice!
EDITED:
After trying hakre's solution, the code worked. Then I moved on to trying to use an extended class of DOMElement, and, as I suspected, it did not work. Here's the new code:
<?php
session_start();
//$_SESSION = array();
$rootTag = 'root';
$doc = new DOMDocument;
if (!isset($_SESSION[$rootTag])) {
$root = new FreezableDOMElement($rootTag);
$doc->appendChild($root);
} else {
$doc->loadXML($_SESSION[$rootTag]);
$root = $doc->documentElement;
}
$child = new FreezableDOMElement('child_element');
$n = $root->appendChild($child);
$ct = 0;
foreach ($root->childNodes as $ch) {
$frozen = $ch->frozen ? 'is frozen' : 'is not frozen';
echo '<br/>'.$ch->tagName.' '.++$ct.': '.$frozen;
//echo '<br/>'.$ch->tagName.' '.++$ct;
}
$_SESSION[$rootTag] = $doc->saveXML();
/**********************************************************************************
* FreezableDOMElement class
*********************************************************************************/
class FreezableDOMElement extends DOMElement {
public $frozen; // boolean value
public function __construct($name) {
parent::__construct($name);
$this->frozen = false;
}
}
?>
It gives me the error Undefined property: DOMElement::$frozen. Like I mentioned in my original post, after saveXML and loadXML, an element originally instantiated with FreezableDOMElement is returning type DOMElement which is why the frozen property is not recognized. Is there any way around this?
You can not store a DOMElement object inside $_SESSION. It will work at first, but with the next request, it will be unset because it can not be serialized.
That's the same like for DOMDocument as you write about in your question.
Store it as XML instead or encapsulate the serialization mechanism.
You are basically facing three problems here:
Serialize the DOMDocument (you do this to)
Serialize the FreezableDOMElement (you do this to)
Keep the private member FreezableDOMElement::$frozen with the document.
As written, serialization is not available out of the box. Additionally, DOMDocument does not persist your FreezableDOMElement even w/o serialization. The following example demonstrates that the instance is not automatically kept, the default value FALSE is returned (Demo):
class FreezableDOMElement extends DOMElement
{
private $frozen = FALSE;
public function getFrozen()
{
return $this->frozen;
}
public function setFrozen($frozen)
{
$this->frozen = (bool)$frozen;
}
}
class FreezableDOMDocument extends DOMDocument
{
public function __construct()
{
parent::__construct();
$this->registerNodeClass('DOMElement', 'FreezableDOMElement');
}
}
$doc = new FreezableDOMDocument();
$doc->loadXML('<root><child></child></root>');
# own objects do not persist
$doc->documentElement->setFrozen(TRUE);
printf("Element is frozen (should): %d\n", $doc->documentElement->getFrozen()); # it is not (0)
As PHP does not so far support setUserData (DOM Level 3), one way could be to store the additional information inside a namespaced attribute with the element. This can also be serialized by creating the XML string when serializing the object and loading it when unserializing (see Serializable). This then solves all three problems (Demo):
class FreezableDOMElement extends DOMElement
{
public function getFrozen()
{
return $this->getFrozenAttribute()->nodeValue === 'YES';
}
public function setFrozen($frozen)
{
$this->getFrozenAttribute()->nodeValue = $frozen ? 'YES' : 'NO';
}
private function getFrozenAttribute()
{
return $this->getSerializedAttribute('frozen');
}
protected function getSerializedAttribute($localName)
{
$namespaceURI = FreezableDOMDocument::NS_URI;
$prefix = FreezableDOMDocument::NS_PREFIX;
if ($this->hasAttributeNS($namespaceURI, $localName)) {
$attrib = $this->getAttributeNodeNS($namespaceURI, $localName);
} else {
$this->ownerDocument->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $prefix, $namespaceURI);
$attrib = $this->ownerDocument->createAttributeNS($namespaceURI, $prefix . ':' . $localName);
$attrib = $this->appendChild($attrib);
}
return $attrib;
}
}
class FreezableDOMDocument extends DOMDocument implements Serializable
{
const NS_URI = '/frozen.org/freeze/2';
const NS_PREFIX = 'freeze';
public function __construct()
{
parent::__construct();
$this->registerNodeClasses();
}
private function registerNodeClasses()
{
$this->registerNodeClass('DOMElement', 'FreezableDOMElement');
}
/**
* #return DOMNodeList
*/
private function getNodes()
{
$xp = new DOMXPath($this);
return $xp->query('//*');
}
public function serialize()
{
return parent::saveXML();
}
public function unserialize($serialized)
{
parent::__construct();
$this->registerNodeClasses();
$this->loadXML($serialized);
}
public function saveBareXML()
{
$doc = new DOMDocument();
$doc->loadXML(parent::saveXML());
$xp = new DOMXPath($doc);
foreach ($xp->query('//#*[namespace-uri()=\'' . self::NS_URI . '\']') as $attr) {
/* #var $attr DOMAttr */
$attr->parentNode->removeAttributeNode($attr);
}
$doc->documentElement->removeAttributeNS(self::NS_URI, self::NS_PREFIX);
return $doc->saveXML();
}
public function saveXMLDirect()
{
return parent::saveXML();
}
}
$doc = new FreezableDOMDocument();
$doc->loadXML('<root><child></child></root>');
$doc->documentElement->setFrozen(TRUE);
$child = $doc->getElementsByTagName('child')->item(0);
$child->setFrozen(TRUE);
echo "Plain XML:\n", $doc->saveXML(), "\n";
echo "Bare XML:\n", $doc->saveBareXML(), "\n";
$serialized = serialize($doc);
echo "Serialized:\n", $serialized, "\n";
$newDoc = unserialize($serialized);
printf("Document Element is frozen (should be): %s\n", $newDoc->documentElement->getFrozen() ? 'YES' : 'NO');
printf("Child Element is frozen (should be): %s\n", $newDoc->getElementsByTagName('child')->item(0)->getFrozen() ? 'YES' : 'NO');
It's not really feature complete but a working demo. It's possible to obtain the full XML without the additional "freeze" data.

Entering Doctypes

I have a Document class that is responsible for holding the document that will eventually be gzipped and then printed onto the page. In that Document holds the document's DocType. The Problem I am facing is how to correctly implement displaying the Doctype.
For example: I could create an array of popular Doctypes that are the most frequently used, and then if you enter 'strict', it would automatically pick HTML 4.01 Strict. If there isn't a known DocType, it could assume you are entering in a custom DocType, and just display what you've entered. This seems a error prone and clumbsy.
$document->setDoctype("strict");
..... in a class far, far, away........
function setDocType($type)
{
if(in_array($type, $this->doctypes))
{
$this->doctype = $this->doctypes[$type];
}
else
{
$this->doctype = $type;
}
}
Another way to do it would be to simply have to enter in the entire DocType every time.
$document->setDoctype('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">');
How would you deal with this problem?
It depends if you want to allow entering custom doctypes. If you consider that situation an exception you should probably throw an exception when someone tries using undefined doctype. You could also provide a method for defining a custom doctype and force using that method before setting the actual doctype.
I would do something like this:
class Document {
private $doctypes = array(
'html4' => '...',
'xhtml1strict' => '...'
);
private $doctype = null;
public function addCustomDocType($name, $definition) {
if (empty($this->doctypes[$name])) {
$this->doctypes[$name] = $definition;
} else {
throw new Exception('Definition already exists');
}
}
public function setDocType($name) {
if (!empty($this->doctypes[$name])) {
$this->doctype = $this->doctypes[$name];
} else {
throw new Exception('Doctype not found');
}
}
}
The second solution where you must type doctype definition is IMO ugly and I would avoid it.
I think you should just checkout the doctype w3c list.
http://www.w3.org/QA/2002/04/valid-dtd-list.html
Are users allowed to enter a custom doctype?
Perhaps create a base class
Doc
And extend it to each type, ex:
StrictDoc extends Doc
etc.
And it'll set its own doc type for each. If you want to add to the doc types, simply create another class that extends Doc.

Categories