insert child to XML that is saved as a String - php

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
);
}

Related

PHP DOMDocument: how to incorporate a string of xml?

I am building up an xml file and need to include a segment of xml saved in a database (yeah, I wish that wasn't the case too).
// parent element
$parent = $dom->createElement('RecipeIngredients');
// the xml string I want to include
$xmlStr = $row['ingredientSectionXml'];
// load xml string into domDocument
$dom->loadXML( $xmlStr );
// add all Ingredient Sections from xmlStr as children of $parent
$xmlList = $dom->getElementsByTagName( 'IngredientSection' );
for ($i = $xmlList->length; --$i >= 0; ) {
$elem = $xmlList->item($i);
$parent->appendChild( $elem );
}
// add the parent to the $dom doc
$dom->appendChild( $parent );
Right now, I get the following error when I hit the line $parent->appendChild( $elem );
Fatal error: Uncaught exception 'DOMException' with message 'Wrong Document Error'
The XML in the string might look something like the following example. An important point is that there may be multiple IngredientSections, all of which need to be appended to the $parent element.
<IngredientSection name="Herbed Cheese">
<RecipeIngredient>
<Quantity>2</Quantity>
<Unit>cups</Unit>
<Item>yogurt cheese</Item>
<Note>(see Tip)</Note>
<MeasureType/>
<IngredientBrand/>
</RecipeIngredient>
<RecipeIngredient>
<Quantity>2</Quantity>
<Unit/>
<Item>scallions</Item>
<Note>, trimmed and minced</Note>
<MeasureType/>
<IngredientBrand/>
</RecipeIngredient>
<IngredientSection name="Cracked-Wheat Crackers">
</IngredientSection>
<RecipeIngredient>
<Quantity>2</Quantity>
<Unit>teaspoon</Unit>
<Item>salt</Item>
<Note/>
<MeasureType/>
<IngredientBrand/>
</RecipeIngredient>
<RecipeIngredient>
<Quantity>1 1/4</Quantity>
<Unit>cups</Unit>
<Item>cracked wheat</Item>
<Note/>
<MeasureType/>
<IngredientBrand/>
</RecipeIngredient>
</IngredientSection>
Here a two possible solutions:
Import From A Source Document
This works only if the XML string is a valid document. You need to import the document element, or any descendant of it. Depends on the part you would like to add to the target document.
$xml = "<child>text</child>";
$source = new DOMDocument();
$source->loadXml($xml);
$target = new DOMDocument();
$root = $target->appendChild($target->createElement('root'));
$root->appendChild($target->importNode($source->documentElement, TRUE));
echo $target->saveXml();
Output:
<?xml version="1.0"?>
<root><child>text</child></root>
Use A Document Fragment
This works for any valid XML fragment. Even if it has no root node.
$xml = "text<child>text</child>";
$target = new DOMDocument();
$root = $target->appendChild($target->createElement('root'));
$fragment = $target->createDocumentFragment();
$fragment->appendXml($xml);
$root->appendChild($fragment);
echo $target->saveXml();
Output:
<?xml version="1.0"?>
<root>text<child>text</child></root>
You need to use ->importNode() instead of ->appendChild(). Your XML snippets are coming from a completely different XML document, and appendChild will only accept nodes which are part of the SAME xml tree. importNode() will accept "foreign" nodes and incorporate them into the main tree.

Deleting an XML Node within a parent

I have an XML file with a root, a parent node and some inner nodes. If I use DOM->load(myxmlfile.xml) is it possible to traverse through the nodes and remove the imageurlnode?
I have tried this example:
$doc = new DOMDocument;
$doc->load('myxmlfile.xml');
$book = $doc->documentElement;
// we retrieve the chapter and remove it from the book
$node = $book->getElementsByTagName('imageurl')->item(0);
$oldnode = $book->removeChild($chapter);
echo $doc->saveXML();
But this only removes items from underneath root. My XML has the following structure:
<root>
<property>
<imageurl></imageurl>
</property
</root>
So when it comes to removing the imgurl node (as it's inside a parent inside the root) how would I remove it?
If you have the node, say $chapter, just use its parent to delete it
$chapter->parentNode->removeChild($chapter);

PHP DOM Cut xml in to pieces and save each child with parent separately

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

PHP: How to replace existing XML node with XMLWriter

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>

How to remove an HTML element using the DOMDocument class

Is there a way to remove a HTML element by using the DOMDocument class?
In addition to Dave Morgan's answer you can use DOMNode::removeChild to remove child from list of children:
Removing a child by tag name
//The following example will delete the table element of an HTML content.
$dom = new DOMDocument();
//avoid the whitespace after removing the node
$dom->preserveWhiteSpace = false;
//parse html dom elements
$dom->loadHTML($html_contents);
//get the table from dom
if($table = $dom->getElementsByTagName('table')->item(0)) {
//remove the node by telling the parent node to remove the child
$table->parentNode->removeChild($table);
//save the new document
echo $dom->saveHTML();
}
Removing a child by class name
//same beginning
$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->loadHTML($html_contents);
//use DomXPath to find the table element with your class name
$xpath = new DomXPath($dom);
$classname='MyTableName';
$xpath_results = $xpath->query("//table[contains(#class, '$classname')]");
//get the first table from XPath results
if($table = $xpath_results->item(0)){
//remove the node the same way
$table ->parentNode->removeChild($table);
echo $dom->saveHTML();
}
Resources
http://us2.php.net/manual/en/domnode.removechild.php
How to delete element with DOMDocument?
How to get full HTML from DOMXPath::query() method?
http://us2.php.net/manual/en/domnode.removechild.php
DomDocument is a DomNode.. You can just call remove child and you should be fine.
EDIT: Just noticed you were probably talking about the page you are working with currently. Don't know if DomDocument would work. You may wanna look to use javascript at that point (if its already been served up to the client)

Categories