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);
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
);
}
Based on the following XML I want to retrieve the product name and image of a certain node (ID)
<?xml version="1.0" encoding="utf-8"?>
<clients>
<client id="A">
<product>Name of product A</product>
<image>Product image name A</image>
</client>
<client id="B">
<product>Name of product B</product>
<image>Product image name B</image>
</client>
</clients>
This is the PHP Code:
$doc = DOMDocument::load('clients.xml');
$xpath = new DOMXPath($doc);
$query = '//client[#id="A"]';
$info = $xpath->query($query);
If I do $info->item(0)->nodeValue I get both information together and not individually:
Name of product A
Product image name A
But I want to get ->product->nodeValue and image->nodeValue based on the client ID.
How can I do that? Doing $info->item(0)->product->nodeValue for example doesn't work.
->item(0)->nodeValue gives everything inside that particular item (in this case 0)
You use XPath to fetch the nodes. The result of an XPath location path is a list of node. ->item(0) returns the first node in this list.This is the client element node.
Calling DOMElement:$nodeValue always returns all descendent text nodes as a string. In you case the text nodes inside the product and the image element nodes.
You will have the to fetch the child nodes if you want the values separately.
$dom = new DOMDocument();
$dom->loadXML($xml);
$xpath = new DOMXpath($dom);
foreach ($xpath->evaluate('//client[#id="A"]/*[self::product or self::image]') as $child) {
echo $child->localName, ': ', $child->nodeValue, "\n";
}
Output:
product: Name of product A
image: Product image name A
Your result returns a single node (client). The nodevalue of that node is all of the text underneath it.
Try:
"//client [#id='A']/*[name()='product' or name()='image']" would return two nodes. Remember to check the name if you want to use them by position.
I want to completely remove the size="id" attribute from every <door> element.
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249" size="30"/>
<door id="1041" entry="6523" size="3094"/>
-- and 1000 more....
</doors>
The PHP code:
$xml = new SimpleXMLElement('http://mysite/doors.xml', NULL, TRUE);
$ids_to_delete = array( 1, 1506 );
foreach ($ids_to_delete as $id) {
$result = $xml->xpath( "//door[#size='$id']" );
foreach ( $result as $node ) {
$dom = dom_import_simplexml($node);
$dom->parentNode->removeChild($dom);
}
}
$xml->saveXml();
I get no errors but it does not delete the size attribute. Why?
I get no errors but it does not delete the size attribute. Why?
There are mulitple reasons why it does not delete the size attribute. The one that popped first into my mind was that attributes are no child nodes. Using a method to remove a child does just not fit to remove an attribute.
Each element node has an associated set of attribute nodes; the element is the parent of each of these attribute nodes; however, an attribute node is not a child of its parent element.
From: Attribute Nodes - XML Path Language (XPath), bold by me.
However, you don't see an error here, because the $result you have is an empty array. You just don't select any nodes to remove - neither elements nor attributes - with your xpath. That is because there is no such element you look for:
//door[#size='1']
You're searching for the id in the size attribute: No match.
These are the reasons why you get no errors and it does not delete any size attribute: 1.) you don't delete attributes here, 2.) you don't query any elements to delete attributes from.
How to delete attributes in SimpleXML queried by Xpath?
You can remove the attribute nodes by selecting them with an Xpath query and then unset the SimpleXMLElement self-reference:
// all size attributes of all doors
$result = $xml->xpath("//door/#size");
foreach ($result as $node) {
unset($node[0]);
}
In this example, all attribute nodes are queried by the Xpath expressions that are size attributes of door elements (which is what you ask for in your question) and then those are removed from the XML.
//door/#size
(see Abbreviated Syntax)
Now here the full example:
<?php
/**
* #link https://eval.in/215817
*/
$buffer = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249" size="30"/>
<door id="1041" entry="6523" size="3094"/>
-- and 1000 more....
</doors>
XML;
$xml = new SimpleXMLElement($buffer);
// all size attributes of all doors
$result = $xml->xpath("//door/#size");
foreach ($result as $node) {
unset($node[0]);
}
$xml->saveXml("php://output");
Output (Online Demo):
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249"/>
<door id="1041" entry="6523"/>
-- and 1000 more....
</doors>
You can do your whole query in DOMDocument using DOMXPath, rather than switching between SimpleXML and DOM:
$dom = new DOMDocument;
$dom->load('my_xml_file.xml');
# initialise an XPath object to act on the $dom object
$xp = new DOMXPath( $dom );
# run the query
foreach ($xp->query( "//door[#size]" ) as $door) {
# remove the attribute
$door->removeAttribute('size');
}
print $dom->saveXML();
Output for the input you supplied:
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249"/>
<door id="1041" entry="6523"/>
</doors>
If you do want only to remove the size attribute for the IDs in your list, you should use the code:
foreach ($ids_to_delete as $id) {
# searches for elements with a matching ID and a size attribute
foreach ($xp->query("//door[#id='$id' and #size]") as $door) {
$door->removeAttribute('size');
}
}
Your code wasn't working for several reasons:
it looks like your XPath was wrong, since your array is called $ids_to_delete and your XPATH is looking for door elements with the size attribute equal to the value from $ids_to_delete;
you're converting the nodes to DOMDocument objects ($dom = dom_import_simplexml($node);) to do the deletion, but $xml->saveXml();, which I presume you printed somehow, is a SimpleXML object;
you need to remove the element attribute; removeChild removes the whole element.
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 have 2 "DOMDocument" objects - $original and $additional. What I want is to take all children from $additional DOMDocument and append it to the end of $original document.
My plan was to take root element of the $additional document. I tried to use:
$root = $additional->documentElement;
$original->appendChild($root)
But I receive error that appendChild expect DOMNode object as an argument.
I tried to access each children of the document through:
$additional->childNodes->item(0);
But it returns object of DOMElement. Can you advice how to get object of DOMNode class? What is the most convenient way to provide this import operation?
$original XML looks like:
<?xml version="1.0" encoding="utf-8"?>
<Product>
<RecordReference>345345</RecordReference>
<NotificationType>03</NotificationType>
<NumberOfPages>100</NumberOfPages
</Product>
$additional XML looks like:
<?xml version="1.0" encoding="utf-8"?>
<MainSubject>
<SubjectScheme>10</SubjectScheme>
</MainSubject>
What I want to have:
<?xml version="1.0" encoding="utf-8"?>
<Product>
<RecordReference>345345</RecordReference>
<NotificationType>03</NotificationType>
<NumberOfPages>100</NumberOfPages>
<MainSubject>
<SubjectScheme>10</SubjectScheme>
</MainSubject>
</Product>
A DOMElement is a DOMNode, DOMNode is the superclass. Here are several child classes for element, text and other nodes. Just iterate, import and append them.
$targetDom = new DOMDocument();
$targetDom->loadXML('<root/>');
$sourceDom = new DOMDocument();
$sourceDom->loadXml('<items><child/>TEXT</items>');
foreach ($sourceDom->documentElement->childNodes as $child) {
$targetDom->documentElement->appendChild(
$targetDom->importNode($child, TRUE)
);
}
This works with the document element, too.
$targetDom = new DOMDocument();
$targetDom->loadXML('<root/>');
$sourceDom = new DOMDocument();
$sourceDom->loadXml('<items><child/>TEXT</items>');
$targetDom->documentElement->appendChild(
$targetDom->importNode($sourceDom->documentElement, TRUE)
);
echo $targetDom->saveXml();
DOMDocument::importNode() creates a copy of the provided node in the context of the document. Only nodes belonging to a document can be appended to it.