I'm relatively new to parsing XML files and am attempting to read a large XML file with XMLReader.
<?xml version="1.0" encoding="UTF-8"?>
<ShowVehicleRemarketing environment="Production" lang="en-CA" release="8.1-Lite" xsi:schemaLocation="http://www.starstandards.org/STAR /STAR/Rev4.2.4/BODs/Standalone/ShowVehicleRemarketing.xsd">
<ApplicationArea>
<Sender>
<Component>Component</Component>
<Task>Task</Task>
<ReferenceId>w5/cron</ReferenceId>
<CreatorNameCode>CreatorNameCode</CreatorNameCode>
<SenderNameCode>SenderNameCode</SenderNameCode>
<SenderURI>http://www.example.com</SenderURI>
<Language>en-CA</Language>
<ServiceId>ServiceId</ServiceId>
</Sender>
<CreationDateTime>CreationDateTime</CreationDateTime>
<Destination>
<DestinationNameCode>example</DestinationNameCode>
</Destination>
</ApplicationArea>
...
I am recieving the following error
ErrorException [ Warning ]: XMLReader::read() [xmlreader.read]: compress.zlib://D:/WebDev/example/local/public/../upload/example.xml.gz:2: namespace error : Namespace prefix xsi for schemaLocation on ShowVehicleRemarketing is not defined
I've searched around and can't find much useful information on using XMLReader to read XML files with namespaces -- How would I go about defining a namespace, if that is in fact what I need to do.. little help? links to pertinent resources?
There needs to be a definition of the xsi namespace. E.g.
<ShowVehicleRemarketing
environment="Production"
lang="en-CA"
release="8.1-Lite"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.starstandards.org/STAR/STAR/Rev4.2.4/BODs/Standalone/ShowVehicleRemarketing.xsd"
>
Update: You could write a user defined filter and then let the XMLReader use that filter, something like:
stream_filter_register('darn', 'DarnFilter');
$src = 'php://filter/read=darn/resource=compress.zlib://something.xml.gz';
$reader->open($src);
The contents read by the compress.zlib wrapper is then "routed" through the DarnFilter which has to find the (first) location where it can insert the xmlns:xsi declaration. But this is quite messy and will take some afford to do it right (e.g. theoretically bucket A could contain xs, bucket B i:schem and bucket C aLocation=")
Update 2: here's an ad-hoc example of a filter in php that inserts the xsi namespace declaration. Mostly untested (worked with the one test I ran ;-) ) and undocumented. Take it as a proof-of-concept not production-code.
<?php
stream_filter_register('darn', 'DarnFilter');
$src = 'php://filter/read=darn/resource=compress.zlib://d:/test.xml.gz';
$r = new XMLReader;
$r->open($src);
while($r->read()) {
echo '.';
}
class DarnFilter extends php_user_filter {
protected $buffer='';
protected $status = PSFS_FEED_ME;
public function filter($in, $out, &$consumed, $closing)
{
while ( $bucket = stream_bucket_make_writeable($in) ) {
$consumed += $bucket->datalen;
if ( PSFS_PASS_ON == $this->status ) {
// we're already done, just copy the content
stream_bucket_append($out, $bucket);
}
else {
$this->buffer .= $bucket->data;
if ( $this->foo() ) {
// first element found
// send the current buffer
$bucket->data = $this->buffer;
$bucket->datalen = strlen($bucket->data);
stream_bucket_append($out, $bucket);
$this->buffer = null;
// no need for further processing
$this->status = PSFS_PASS_ON;
}
}
}
return $this->status;
}
/* looks for the first (root) element in $this->buffer
* if it doesn't contain a xsi namespace decl inserts it
*/
protected function foo() {
$rc = false;
if ( preg_match('!<([^?>\s]+)\s?([^>]*)>!', $this->buffer, $m, PREG_OFFSET_CAPTURE) ) {
$rc = true;
if ( false===strpos($m[2][0], 'xmlns:xsi') ) {
echo ' inserting xsi decl ';
$in = '<'.$m[1][0]
. ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
. $m[2][0] . '>';
$this->buffer = substr($this->buffer, 0, $m[0][1])
. $in
. substr($this->buffer, $m[0][1] + strlen($m[0][0]));
}
}
return $rc;
}
}
Update 3: And here's an ad-hoc solution written in C#
XmlNamespaceManager nsmgr = new XmlNamespaceManager(new NameTable());
// prime the XMLReader with the xsi namespace
nsmgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
using ( XmlReader reader = XmlTextReader.Create(
new GZipStream(new FileStream(#"\test.xml.gz", FileMode.Open, FileAccess.Read), CompressionMode.Decompress),
new XmlReaderSettings(),
new XmlParserContext(null, nsmgr, null, XmlSpace.None)
)) {
while (reader.Read())
{
System.Console.Write('.');
}
}
You can file_get_contents and str_replace the XML before passing it to XMLReader.
Either insert the required namespace declararation for the xsi prefix:
$reader = new XMLReader;
$reader->xml(str_replace(
'<ShowVehicleRemarketing',
'<ShowVehicleRemarketing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
file_get_contents('http://example.com/data.xml')));
Another option would be to remove the schemaLocation attribute:
$reader->xml(str_replace(
'xsi:schemaLocation="http://www.starstandards.org/STAR /STAR/Rev4.2.4/BODs/Standalone/ShowVehicleRemarketing.xsd"',
'',
file_get_contents('http://example.com/data.xml')));
However, if there is more prefixes in the document, you will have to replace all of them.
Either fix whatever's writing out malformed XML, or write a separate tool to perform the fix later. (It doesn't have to read it all into memory at the same time, necessarily - stream the data in/out, perhaps reading and writing a line at a time.)
That way your reading code doesn't need to worry about trying to do something useful with the data and fixing it up at the same time.
The xsi namespace is normally reserved for use with Schema Instance Namespace:
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
if it isn't, your XML file is not XML+NS compliant and cannot be parsed. So you should solve that in the source document.
A note on xsi: it is even more vital than some possible other namespaces, because it directs a validating parser to the correct schema locations for the schema of your XML.
Related
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)!
I'm sending the following XML to an api using cURL:
$xml = "<request type='auth' timestamp='$timestamp'>
<merchantid>$merchantid</merchantid>
<account>$account</account>
<orderid>$orderid</orderid>
<amount currency='$currency'>$amount</amount>
<card>
<number>$cardnumber</number>
<expdate>$expdate</expdate>
<type>$cardtype</type>
<chname>$cardname</chname>
</card>
<sha1hash>$sha1hash</sha1hash>
</request>";
What is the best way to avoid hard coding this XML? I was thinking of using XMLWriter but seems strange as it won't be changing.
Should I use a template? Or generate it using XMLWriter / Simple XML?
As I mentioned in the comments, there's not necessarily a right answer to this but I recently had to write a project around an XML API Feed as well. I decided to go with XMLWriter and it's still very easy to interchange into others easily by using their respected .loadXML() functions.
class SomeApi extends XMLwriter {
public function __construct() {
$this->openMemory();
$this->setIndent( true );
$this->setIndentString ( " " );
$this->startDocument( '1.0', 'UTF-8', 'no' );
$this->startElement( 'root' );
}
public function addNode( $Name, $Contents ) {
$this->startElement( $Name );
$this->writeCData( $Contents );
$this->endElement();
}
public function output() {
$this->endElement();
$this->endDocument();
}
//Returns a String of Xml.
public function render() {
return $this->outputMemory();
}
}
$newRequest = new SomeApi();
$newRequest->addNode( 'some', 'Some Lots of Text' );
$Xml = $newRequest->render();
I think it's a nice clean way writing an XML Feed in PHP, furthermore as you can add internal functions such as:
$this->addHeader();
private function addHeader() {
$this->addNode( 'login', 'xxxxx' );
$this->addNode( 'password', 'xxxxx' );
}
Which then appends nodes that you'll use over & over again. Then if you suddenly need to use a DOMDocument object for example (As I needed too for XSL).
$Dom = new DOMDocument();
$Dom->loadXML( $Xml );
Should I use a template?
You actually already did use a template here.
Or generate it using XMLWriter / Simple XML?
XMLWriter and also SimpleXMLElement are components that allow you to create XML easily. For your specific case I'd use SimpleXML for a start:
$xml = new SimpleXMLElement('<request type="auth"/>');
$xml['timestamp'] = $timestamp;
$xml->merchantid = $merchantid;
$xml->account = $account;
$xml->orderid = $orderid;
$xml->addChild('amount', $amount)['currency'] = $currency;
$card = $xml->addChild('card');
$card->number = $cardnumber;
$card->expdate = $expdate;
$card->type = $cardtype;
$card->chname = $cardname;
$xml->sha1hash = $sha1hash;
See that the XML is not hardcoded any longer, only the names used are. The SimpleXML library takes care to create the XML (demo, here the output is beautified for better readability):
<?xml version="1.0"?>
<request type="auth" timestamp="">
<merchantid></merchantid>
<account></account>
<orderid></orderid>
<amount currency=""/>
<card>
<number></number>
<expdate></expdate>
<type></type>
<chname></chname>
</card>
<sha1hash></sha1hash>
</request>
Thanks to the library, the output is always valid XML and you don't need to care about the details here. You can further simplify it by wrapping it more, but I don't think this is of much use with your very little XML you have here.
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()
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.
I'm working on a new class to wrap XML handling. I want my class to use simplexml if it's installed, and the built in XML functions if it's not. Can anyone give me some suggestions on a skeleton class to do this? It seems "wrong" to litter each method with a bunch of if statements, and that also seems like it would make it nearly impossible to correctly test.
Any upfront suggestions would be great!
EDIT: I'm talking about these built-in xml functions.
Which built-in xml functions are you referring to? SimpleXml is a standard extension, which uses libxml underneath - just as the dom extension does. So if the dom extension is installed, chances are that so is SimpleXml.
I've made a class which wraps SimpleXml functionality... take what you may from it...
bXml.class.inc
There is one weird thing... it's that SimpleXml doesn't allow its constructor to be overloaded, so you can't do things at initiation ... like override the input value (i.e. so you can accept XML as in input). I got around that limitation by using an ArrayObject class to wrap the new SimpleXml class.
I use something like this for doing xml translations and content:
Assuming xml structure something like this (important to use a regular structure, means you can pull off some nice agile tricks!):
<word name="nameofitem">
<en>value</en>
<pt>valor</pt>
<de>value_de</de>
</word>
and then a class to handle the xml:
class translations
{
public $xml = null;
private $file = null;
private $dom = null;
function __construct($file="translations") {
// get xml
$this->file = $file;
$this->haschanges = false;
$this->xml = file_get_contents($_SERVER['DOCUMENT_ROOT']."/xml/".$file.".xml");
$this->dom = new DOMdocument();
$this->dom->loadXML($this->xml);
}
function updateNode($toupdate, $newvalue, $lang="pt",$rootnode="word"){
$this->haschanges = true;
$nodes = $this->dom->getElementsByTagName($rootnode);
foreach ($nodes as $key => $value) {
if ($value->getAttribute("name")==$toupdate) {
$nodes->item($key)->getElementsByTagName($lang)->item(0)->nodeValue = htmlspecialchars($newvalue,ENT_QUOTES,'UTF-8');
}
}
}
function saveUpdated(){
$toSave = $this->dom->saveXML();
if ($this->haschanges === true) {
file_put_contents($_SERVER['DOCUMENT_ROOT']."/xml/".$this->file.".xml", $toSave);
return true;
}
else {
return false;
}
}
}
I took out a few of the methods I have, for brevity, but I extend this with things to handle file and image uploads etc too.
Once you have all this you can do:
$xml = new translations();
// loop through all the language posts
foreach ($_POST["xml"]["en"] as $key => $value) {
$xml->updateNode($key, stripslashes($value), "en");
}
Or something ;) hope this gives you some ideas!