I am editing an XML file and need to populate it with data from a database. DOM works but it is unable to scale to several hundreds of MBs so I am now using XMLReader and XMLWriter which can write the very large XML file. Now, I need to select a node and add children to it but I can't find a method to do it, can someone help me out?
I can find the node I need to add children to by:
if ($xmlReader->nodeType == XMLReader::ELEMENT && $xmlReader->name == 'data')
{
echo 'data was found';
$data = $xmlReader->getAttribute('data');
}
How do I now add more nodes/children to the found node? Again for clarification, this code will read and find the node, so that is done. What is required is how to modify the found node specifically? Is there a way with XMLWriter for which I have not found a method that will do that after reading through the class documentation?
Be default the expanded nodes (missing in your question)
$node = $xmlReader->expand();
are not editable with XMLReader (makes sense by that name). However you can make the specific DOMNode editable if you import it into a new DOMDocument:
$doc = new DOMDocument();
$node = $doc->importNode($node);
You can then perform any DOM manipulation the DOM offers, e.g. for example adding a text-node:
$textNode = $doc->createTextNode('New Child TextNode added :)');
$node->appendChild($textNode);
If you prefer SimpleXML for manipulation, you can also import the node into SimpleXML after it has been imported into the DOMDocument:
$xml = simplexml_import_dom($node);
An example from above making use of my xmlreader-iterators that just offer me some nicer interface to XMLReader:
$reader = new XMLReader();
$reader->open($xmlFile);
$elements = new XMLElementIterator($reader, 'data');
foreach ($elements as $element)
{
$node = $element->expand();
$doc = new DOMDocument();
$node = $doc->importNode($node, true);
$node->appendChild($doc->createTextNode('New Child TextNode added :)'));
echo $doc->saveXML($node), "\n";
}
With the following XML document:
<xml>
<data/>
<boo>
<blur>
<data/>
<data/>
</blur>
</boo>
<data/>
</xml>
The small example code above produces the following output:
<data>New Child TextNode added :)</data>
<data>New Child TextNode added :)</data>
<data>New Child TextNode added :)</data>
<data>New Child TextNode added :)</data>
Related
I'm working in PHP, I have a large XML saved in a String,I want to insert as a first child a node, I know the name of the parent, is something like:
<mytag Someattributte="anything">
here I want to put my tag
...
a lot of tags
...
</mytag>
How can I do that?
With DOM you use Xpath to fetch nodes, DOM document methods to create new nodes (DOMDocument::createDocumentFragment()) and DOM node methods to insert/append them (DOMDocument::insertBefore()).
Document fragments are a construct that allows you to treat a list of nodes as a single node. And they can load an XML fragment string.
$targetXml = <<<'XML'
<mytag Someattribute="anything">
here I want to put my tag
...
a lot of tags
...
</mytag>
XML;
$fragmentXml = <<<'XML'
<othertag>with text</othertag>
XML;
$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);
// fetch the first mytag node that has a Someattribute
foreach ($xpath->evaluate('//mytag[#Someattribute][1]') as $targetNode) {
// create a new fragment
$fragment = $document->createDocumentFragment();
// append the stored xml string to the fragment node
$fragment->appendXml($fragmentXml);
// insert the fragment before the first child of the target node
$targetNode->insertBefore($fragment, $targetNode->firstChild);
}
echo $document->saveXml();
Output:
<?xml version="1.0"?>
<mytag Someattribute="anything"><othertag>with text</othertag>
here I want to put my tag
...
a lot of tags
...
</mytag>
If you XML string is a whole document you need to load it as a separate document instance and import the document element.
foreach ($xpath->evaluate('//mytag[#Someattribute][1]') as $targetNode) {
$import = new DOMDocument();
$import->loadXml($fragmentXml);
$targetNode->insertBefore(
$document->importNode($import->documentElement, TRUE),
$targetNode->firstChild
);
}
I'm using this in SimpleXML and PHP:
foreach ($xml->children() as $node) {
echo $node->attributes('namespace')->id;
}
That prints the id attribute of all nodes (using a namespace).
But now I want to know the line number that $node is located in the XML file.
I need the line number, because I'm analyzing the XML file, and returning to the user information of possible issues to resolve them. So I need to say something like: "Here you have an error at line X". I'm sure that the XML file would be in a standard format that will have enough line breaks for this to be useful.
It is possible with DOM. DOMNode provides the function getLineNo().
DOM
$xml = <<<'XML'
<foo>
<bar/>
</foo>
XML;
$dom = new DOMDocument();
$dom->loadXml($xml);
$xpath = new DOMXpath($dom);
var_dump(
$xpath->evaluate('//bar[1]')->item(0)->getLineNo()
);
Output:
int(2)
SimpleXML
SimpleXML is based on DOM, so you can convert SimpleXMLElement objects to DOMElement objects.
$element = new SimpleXMLElement($xml);
$node = dom_import_simplexml($element->bar);
var_dump($node->getLineNo());
And yes, most of the time if you have a problem with SimpleXML, the answer is to use DOM.
XMLReader
XMLReader has the line numbers internally, but here is no direct method to access them. Again you will have to convert it into a DOMNode. It works because both use libxml2. This will read the node and all its descendants into memory, so be careful with it.
$reader = new XMLReader();
$reader->open('data://text/xml;base64,'.base64_encode($xml));
while ($reader->read()) {
if ($reader->nodeType == XMLReader::ELEMENT && $reader->name== 'bar') {
var_dump($reader->expand()->getLineNo());
}
}
I have next type of XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test SYSTEM "dtd">
<root>
<tag1>
<1>Name</1>
<2>Num1</2>
<3>NumOrder</3>
<4>test</5>
<6>line</6>
<7>HTTP </7>
<8>1</8>
<9></9>
</tag1>
<tag2>
<1>Name</1>
<2>Num1</2>
<3>NumOrder</3>
<4>test</5>
<6>line</6>
<7>HTTP </7>
<8>1</8>
<9></9>
</tag2>
...
<tagN>
<1>Name</1>
<2>Num1</2>
<3>NumOrder</3>
<4>test</5>
<6>line</6>
<7>HTTP </7>
<8>1</8>
<9></9>
</tagN>
</root>
And i need to get root with each child element separately in array saved as HTML:
array = [rootwithchild1,rootwithchild2...N];
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test SYSTEM "dtd">
<root>
<tagN>
<1>Name</1>
<2>Num1</2>
<3>NumOrder</3>
<4>test</5>
<6>line</6>
<7>HTTP </7>
<8>1</8>
<9></9>
</tagN>
</root>
For now i make 2 doms, in one i get all child separately, in another i have deleted all child and left only root. At these step i wanted to add each child to root, save as html, delete child, and so on with each child, but this doesn't work.
$bodyNode = $copydoc->getElementsByTagName('root')->item(0);
foreach ($mini as $value) {
$bodyNode->appendChild($value);
$result[] = $copydoc->saveHTML();
$bodyNode->removeChild($value);
}
Error on $bodyNode->appendChild($value);
Mini is array of cut child.
Lib: $doc = new DOMDocument();
Can anyone advice how to do this right, maybe better to use xpath or something else..?
Thanks
I would simply create a new document that contains only the root element and a “fake” initial child:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test SYSTEM "dtd">
<root>
<fakechild />
</root>
After that, loop over the child elements of the original document – and for each of those perform the following steps:
import the child node from the original document into the new document using DOMDocument::importNode
replace the current child node of the root element of the new document with the imported node using DOMNode::replaceChild with the firstChild of the root element as second parameter
save the new document
(Having the <fakechild /> in the root element to begin with is not technically necessary, a simple whitespace text node should do as well – but with an empty root element this would not work in such a straight fashion, because the firstChild would give you NULL in the first loop iteration, so you would not have a node to feed to DOMNode::replaceChild as second parameter. Of course you could do additional checks for that and use appendChild instead of replaceChild for the first item … but why complicate stuff more than necessary.)
DOMNode::getElemementsByTagName() returns a live result. So if you remove the node from the DOM it is removed from the node list as well.
You can iterate the list backwards...
for ($i = $nodes->length - 1; $i >= 0; $i--) {
$node = $nodes->item($i);
...
}
... or copy it to an array:
foreach (iterator_to_array($nodes) as $node) {
...
}
Node lists from DOMXpath::evaluate() are not affected that way. XPath allows a more specific selection of nodes, too.
$xpath = new DOMXpath($domDocument);
$nodes = $xpath->evaluate('/root/*');
foreach (iterator_to_array($nodes) as $node) {
...
}
But I wonder why are you modifying (destroying) the original XML source?
If would create a new document to act as a template and. Never removing nodes, only creating new documents and importing them:
// load the original source
$source= new DOMDocument();
$source->loadXml($xml);
$xpath = new DOMXpath($source);
// create a template dom
$template = new DOMDocument();
$parent = $template;
// add a node and all its ancestors to the template
foreach ($xpath->evaluate('/root/part[1]/ancestor-or-self::*') as $node) {
$parent = $parent->appendChild($template->importNode($node, FALSE));
}
// for each of the child element nodes
foreach ($xpath->evaluate('/root/part/*') as $node) {
// create a new target
$target = new DOMDocument();
// import the nodes from the template
$target->appendChild($target->importNode($template->documentElement, TRUE));
// find the first element node that has no child element nodes
$targetXpath = new DOMXpath($target);
$targetNode = $targetXpath->evaluate('//*[count(*) = 0]')->item(0);
// append the child node from the original xml
$targetNode->appendChild($target->importNode($node, TRUE));
echo $target->saveXml(), "\n\n";
}
Demo: https://eval.in/191304
I'm using PHP/Zend to load html into a DOM, and then I get a specific div id that I want to modify.
$dom = new Zend_Dom_Query($html);
$element = $dom->query('div[id="someid"]');
How do I modify the text/content/html displayed inside that $element div, and then save the changes to the $dom or $html so I can print the modified html. Any idea how to do this?
Zend_Dom_Query is tailored just for querying a dom, so it doesn't provide an interface in and of itself to alter the dom and save it, but it does expose the PHP Native DOM objects that will let you do so. Something like this should work:
$dom = new Zend_Dom_Query($html);
$document = $dom->getDocument();
$elements = $dom->query('div[id="someid"]');
foreach($elements AS $element) {
//$element is an instance of DOMElement (http://www.php.net/DOMElement)
//You have to create new nodes off the document
$node = $document->createElement("div", "contents of div");
$element->appendChild($node)
}
$newHtml = $document->saveXml();
Take a look at the PHP Doc for DOMElement to get an idea of how you can alter the dom:
http://www.php.net/DOMElement
I'd like to search for nodes with the same node name in a SimpleXML Object no matter how deep they are nested and create an instance of them as an array.
In the HTML DOM I can do that with JavaScript by using getElementsByTagName(). Is there a way to do that in PHP as well?
Yes use xpath
$xml->xpath('//div');
Here $xml is your SimpleXML object.
In this example you will get array of all 'div' elements
$fname = dirname(__FILE__) . '\\xml\\crRoll.xml';
$dom = new DOMDocument;
$dom->load($fname, LIBXML_DTDLOAD|LIBXML_DTDATTR);
$root = $dom->documentElement;
$xpath = new DOMXpath($dom);
$xpath->registerNamespace('cr', "http://www.w3.org/1999/xhtml");
$candidateNodes = $xpath->query("//cr:break");
foreach ($candidateNodes as $child) {
$max = $child->getAttribute('tstamp');
}
This finds all the BREAK nodes (tstamp attr) using XPath ...
Only on DOMDocument::getElementsByTagName,
however, you can import/export SimpleXML into DOMDocument,
or simply use DOMDocument to parse XML.
Another answer mentioned about Xpath,
it will return duplication of node, if you have something like :-
<div><div>1</div></div>