I am looking to try and change a XML output so that the element structure changes and some CDATA becomes an attribute rather than an <element>
Given the XML stack.xml:
<root>
<item>
<name>name</name>
<type>Type</type>
<dateMade>Datemade</dateMade>
<desc>Desc</desc>
</item>
....(more Items)...
</root>
I would like to change the XML output to stacksaved.xml:
<root>
<item>
<name>name</name>
<Itemtype type="Type">
<Itemdate dateMade="Datemade">
<desc>Desc</desc>
</Itemdate>
<Itemtype>
</item>
....(next item)....
</root>
So far my PHP DOM looks like this:
<?php
//create and load
$doc = new DOMDocument();
$doc->load('stack.xml');
$types=$doc->getElementsByTagName("type");
foreach ($types as $type)
{
$attribute=$doc->getElementsByTagName("type");
$doc->getElementsByTagName("type").setAttribute("$attribute");
}
$doc->save('stacksaved.xml'); //save the final results into xml file
?>
I keep getting the error: Fatal error: Call to undefined function setAttribute() and the document is not saved or edited in anyway. I am really new to DOM/PHP and would greatly appreciate any advice!
How would I go about changing the child structure and the element to the desired output?
Thanks as always for the read!
EDIT: Parfait gave a great explanation and showed the great power of XSLT but I am trying to get this to run using pure php only as a learning exercise for php/DOM. Can anyone help with converting this using PHP only?
For a pure PHP DOM solution, consider creating a new DOMDocument iterating over values of old document using createElement, appendChild, and setAttribute methods. The multiple nested if logic is needed to check existence of a node before creating elements with items' node values, otherwise Undefined Warnings are raised.
$doc = new DOMDocument();
$doc->load('stack.xml');
// INITIALIZE NEW DOM DOCUMENT
$newdoc = new DOMDocument('1.0', 'UTF-8');
$newdoc->preserveWhiteSpace = false;
$newdoc->formatOutput = true;
// APPEND ROOT
$root= $newdoc->appendChild($newdoc->createElement("root"));
$items=$doc->getElementsByTagName("item");
// ITERATIVELY APPEND ITEM AND CHILDREN
foreach($items as $item){
$ItemNode = $newdoc->createElement("item");
$root->appendChild($ItemNode);
if (count($item->getElementsByTagName("name")->item(0)) > 0) {
$ItemNode->appendChild($newdoc->createElement('name', $item->getElementsByTagName("name")->item(0)->nodeValue));
}
if (count($item->getElementsByTagName("type")->item(0)) > 0) {
$ItemtypeNode = $ItemNode->appendChild($newdoc->createElement('Itemtype'));
$ItemtypeNode->setAttribute("type", $item->getElementsByTagName("type")->item(0)->nodeValue);
if (count($item->getElementsByTagName("dateMade")->item(0)) > 0) {
$ItemdateNode = $ItemtypeNode->appendChild($newdoc->createElement('Itemdate'));
$ItemdateNode->setAttribute("dateMade", $item->getElementsByTagName("dateMade")->item(0)->nodeValue);
if (count($item->getElementsByTagName("desc")->item(0)) > 0) {
$ItemdateNode->appendChild($newdoc->createElement('desc', $item->getElementsByTagName("desc")->item(0)->nodeValue));
}
}
}
}
// ECHO AND SAVE NEW DOC TREE
echo $newdoc->saveXML();
$newdoc->save($cd.'/ItemTypeDateMade_dom.xml');
Output
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item>
<name>name</name>
<Itemtype type="Type">
<Itemdate dateMade="Datemade">
<desc>Desc</desc>
</Itemdate>
</Itemtype>
</item>
</root>
As mentioned in previous answer, here requires for and nested if that would not be required with XSLT. In fact, using microtime, we can compare script runtimes. Below enlargen stack.xml:
$time_start = microtime(true);
...
echo "Total execution time in seconds: " . (microtime(true) - $time_start) ."\n";
At 1,000 node lines, XSLT proves faster than DOM:
# XSLT VERSION
Total execution time in seconds: 0.0062189102172852
# DOM VERSION
Total execution time in seconds: 0.013695955276489
At 2,000 node lines, XSLT still remains about 2X faster than DOM:
# XSLT VERSION
Total execution time in seconds: 0.014697074890137
# DOM VERSION
Total execution time in seconds: 0.031282186508179
At 10,000 node lines, XSLT now becomes slightly faster than DOM. Reason for DOM's catch up might be due to the memory inefficiency XSLT 1.0 maintains for larger files, especially (> 100 MB). But arguably here for this use case, the XSLT approach is an easier PHP script to maintain and read:
# XSLT VERSION
Total execution time in seconds: 0.27568817138672
# DOM VERSION
Total execution time in seconds: 0.37149095535278
Consider XSLT, the special-purpose, declarative language designed to transform XML documents. PHP can run XSLT 1.0 scripts with the php-xsl extension (be sure to enable it in .ini file). With this approach, you avoid any need of foreach looping or if logic.
XSLT (save as .xsl file)
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes" />
<xsl:template match="root">
<xsl:copy>
<xsl:apply-templates select="item"/>
</xsl:copy>
</xsl:template>
<xsl:template match="item">
<xsl:copy>
<xsl:copy-of select="name"/>
<Itemtype type="{type}">
<Itemdate dateMade="{dateMade}">
<xsl:copy-of select="desc"/>
</Itemdate>
</Itemtype>
</xsl:copy>
</xsl:template>
</xsl:transform>
PHP
$doc = new DOMDocument();
$doc->load('stack.xml');
$xsl = new DOMDocument;
$xsl->load('XSLTScript.xsl');
// CONFIGURE TRANSFORMER
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// PROCESS TRANSFORMATION
$newXML = $proc->transformToXML($doc);
// ECHO STRING OUTPUT
echo $newXML;
// SAVE OUTPUT TO FILE
file_put_contents('Output.xml', $newXML);
Output
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item>
<name>name</name>
<Itemtype type="Type">
<Itemdate dateMade="Datemade">
<desc>Desc</desc>
</Itemdate>
</Itemtype>
</item>
</root>
Related
I have two XML files with this structure:
first.xml
<items>
<item>
<id>foo</id>
<desc>lorem ipsum</desc>
</item>
<item>
<id>boo</id>
<desc>lorem ipsum</desc>
</item>
</items>
second.xml
<item_list>
<item id="foo">
<stock_quantity>20</stock_quantity>
</item>
<item id="boo">
<stock_quantity>11</stock_quantity>
</item>
</item_list>
and I need to combine them by the id so the ouput file would look like this:
output.xml
<items>
<item>
<id>foo</id>
<desc>lorem ipsum</desc>
<stock_quantity>20</stock_quantity>
</item>
<item>
<id>boo</id>
<desc>lorem ipsum</desc>
<stock_quantity>11</stock_quantity>
</item>
</items>
I need to use PHP and XML DOMDocument. Do you have any idea how to do this?
You can use simplexml library to achieve that,
// loading xml to object from file
$xml1 = simplexml_load_file("first.xml") or die("Error: Cannot create object");
$xml2 = simplexml_load_file("second.xml") or die("Error: Cannot create object");
// its core xml iterator for simplexml library
foreach ($xml1->children() as $items1) {
$id = trim($items1->id); // trim to check with id matched in 2.xml
foreach ($xml2->children() as $items2) { // iterating children of 2.xml
if ($items2[0]['id'] == $id) { // simply checking attribute of id in 2.xml with 1.xml's id value
foreach ($items2 as $key => $value) {
$items1->addChild($key, (string) ($value)); // adding children to 1.xml object
}
}
}
}
$xml1->asXml('output.xml'); // generating https://www.php.net/manual/en/simplexmlelement.asxml.php
Using DOMDocument and it's ability to copy nodes from one document to the other allows you to directly insert the node from the stock to the main XML.
Rather than looping to find the matching record, this also uses XPath to search for the matching record, the expression //item[#id='boo']/stock_quantity says find the <stock_quantity> element in the <item> element with an attribute of id='boo'
$main = new DOMDocument();
$main->load("main.xml");
$add = new DOMDocument();
$add->load("stock.xml");
$searchAdd = new DOMXPath($add);
// Find the list of items
$items = $main->getElementsByTagName("item");
foreach ( $items as $item ) {
// Exract the value of the id node
$id = $item->getElementsByTagName("id")[0]->nodeValue;
// Find the corresponding node in the stock file
$stockQty = $searchAdd->evaluate("//item[#id='{$id}']/stock_quantity");
// Import the <stock_quantity> node (and all contents)
$copy = $main->importNode($stockQty[0], true);
// Add the imported node
$item->appendChild($copy);
}
echo $main->saveXML();
Consider XSLT, the special-purpose language (like SQL) designed to transform XML files such as your specific end-use needs. Like many general-purpose languages, PHP can run XSLT 1.0 as a lower level layer using special libraries namely php-xsl class (requires the .ini extension enabled).
XSLT (save as .xsl file, a special .xml file; below assumes second XML in same directory)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- IDENTITY TRANSFORM -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- ADD NODE BY CORRESPONDING id VALUE -->
<xsl:template match="item">
<xsl:copy>
<xsl:variable name="curr_id" select="id"/>
<xsl:apply-templates select="#*|node()"/>
<xsl:copy-of select="document('second.xml')/item_list/item[#id = $curr_id]/*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
PHP (reference only first XML)
// Load the XML source and XSLT file
$xml = new DOMDocument;
$xml->load('first.xml');
$xsl = new DOMDocument;
$xsl->load('XSLTScript.xsl');
// Configure transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXML = new DOMDocument;
$newXML = $proc->transformToXML($xml);
echo $newXML;
// Save output to file
$xmlfile = 'output.xml';
file_put_contents($xmlfile, $newXML);
I'm trying to combine multiple XML files that have the same structure into one file.
This is the structure of my XML files:
file1.xml:
<root information="file1">
<items>
<item>FOO</item>
<item>BAR</item>
</items>
</root>
file2.xml:
<root information="file2">
<items>
<item>BAR</item>
<item>FOO</item>
</items>
</root>
Using this code I've been able to combine them:
$files= array(
'file1.xml',
'file2.xml'
);
$dom = new DOMDocument();
$dom->appendChild($dom->createElement('root'));
foreach ($files as $filename) {
$addDom = new DOMDocument();
$addDom->load($filename);
if ($addDom->documentElement) {
foreach ($addDom->documentElement->childNodes as $node) {
$dom->documentElement->appendChild(
$dom->importNode($node, TRUE)
);
}
}
}
$dom->save('output.xml');
This works partly but removes the original root element which has the information attribute that I still need. So I would like to keep all the existing root elements but wrap them in a new root element. This is what I would like to end up with:
<files>
<root information="file1">
<items>
<item>FOO</item>
<item>BAR</item>
</items>
</root>
<root information="file2">
<items>
<item>BAR</item>
<item>FOO</item>
</items>
</root>
</files>
But I can't figure out how to append the file. Whatever I try it only ends up at the bottom of the output file instead of appending all old root elements. I'm guessing this is simple as hell but I just can't figure it out. Help much appreciated!
Actually your source only has some minor mistakes. You create a root document element in the target document, not the files element in you example. Additionally your copy of the nodes in the source documents is a level to deep, you just need to import their document elements.
I modified your code a little to make it self contained and fixed the mistakes.
$files= array(
'file1.xml' =>
'<root information="file1">
<items>
<item>FOO</item>
<item>BAR</item>
</items>
</root>',
'file2.xml' =>
'<root information="file2">
<items>
<item>BAR</item>
<item>FOO</item>
</items>
</root>'
);
// create a target document with a files root
$target = new DOMDocument();
$target->appendChild($target->createElement('files'));
// iterate the source files array
foreach ($files as $name => $content) {
// load each source
$source = new DOMDocument();
$source->loadXml($content);
// if it has a document element
if ($source->documentElement) {
// copy it to the target document
$target->documentElement->appendChild(
$target->importNode($source->documentElement, TRUE)
);
}
}
$target->formatOutput = TRUE;
echo $target->saveXml();
Consider XSLT the special-purpose language designed to transform XML files. XSLT maintains the document() function to parse from external files at paths relative to script. PHP can run XSLT 1.0 scripts with its php-xsl class. Be sure to enable this extension in .ini file.
Should your files be very numerous such as hundreds, consider building the XSLT script on the fly in PHP loop. As information, XSLT is a well-formed XML file, so can be parsed from file or string.
XSLT (save as .xsl file in same directory as all XML files)
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<files>
<xsl:copy-of select="root"/>
<xsl:copy-of select="document('file2.xml')/root"/>
<xsl:copy-of select="document('file3.xml')/root"/>
<xsl:copy-of select="document('file4.xml')/root"/>
<!-- add more as needed -->
</files>
</xsl:template>
</xsl:stylesheet>
PHP (load first XML and XSL scripts, then transform/output)
// LOAD XML SOURCE
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->load('file1.xml'); // ONLY LOAD FIRST XML
// LOAD XSL SOURCE
$xsl = new DOMDocument('1.0', 'UTF-8');
$xsl->load('XSLT_Script.xsl');
// TRANSFORM XML
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
$newXML = $proc->transformToXML($xml);
// SAVE NEW XML
file_put_contents('Output.xml', $newXML);
I have an XML file and has 300 elements. I just want to pull 10 latest records from it and create another XML file.
I will really appreciate if you can just give me some ideas about it?
PHP
$file = '/directory/xmlfile.xml';
if(!$xml = simplexml_load_file($file)){
exit('Failed to open '.$file);
} else{
print_r($xml);
// I want to do some logic here to retrieve top 10 records from file and then create another xml file with 10 records
}
XML Sample Data
<data>
<total>212</total>
<start>0</start>
<count>212</count>
<data>
<item0>
<id>123</id>
<title>abc-test1</title>
<clientContact>
<id>111</id>
<firstName>abc</firstName>
<lastName>xyz</lastName>
<email>abc#xyz.ca</email>
</clientContact>
<isOpen>1</isOpen>
<isPublic>1</isPublic>
<isJobcastPublished>1</isJobcastPublished>
<owner>
<id>222</id>
<firstName>testname</firstName>
<lastName>testlastname</lastName>
<address>
<address1>test address,</address1>
<address2>test</address2>
<city>City</city>
<state>state</state>
<zip>2222</zip>
<countryID>22</countryID>
<countryName>Country</countryName>
<countryCode>ABC</countryCode>
</address>
<email>test#test.com</email>
<customText1>test123</customText1>
<customText2>testxyz</customText2>
</owner>
<publicDescription>
<p>test info</p>
</publicDescription>
<status>test</status>
<dateLastModified>22222</dateLastModified>
<customText4>test1</customText4>
<customText10>test123</customText10>
<customText11>test</customText11>
<customText16>rtest</customText16>
<_score>123</_score>
</item0>
<item1>
...
</item1>
...
</data>
</data>
Consider XSLT, the special-purpose language designed to transform/manipulate XML to various end uses like extracting top ten <item*> tags. No need of foreach or if logic. PHP maintains an XSLT processor that can be enabled in .ini file (php-xsl).
Specifically, XSLT runs the Identity Transform to copy document as is then writes a blank template for item nodes with position over 10. XML was a bit difficult due to same parent/child <data> tags.
XSLT (save as .xsl file which is a well-formed xml)
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[substring(name(),1,4)='item' and position() > 10]"/>
</xsl:stylesheet>
PHP
$file = '/directory/xmlfile.xml';
if(!$xml = simplexml_load_file($file)) {
exit('Failed to open '.$file);
} else {
// Load XSLT
$xsl = new DOMDocument;
$xsl->load('/path/to/xsl_script.xsl');
// Configure transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXML = new DOMDocument;
$newXML = $proc->transformToXML($xml);
// Echo new XML tree
echo $newXML;
// Save output to file
$xmlfile = '/path/to/output.xml';
file_put_contents($xmlfile, $newXML);
}
I have an XML document that looks like this:
<root>
<node/>
<node>
<sub>more</sub>
</node>
<node>
<sub>another</sub>
</node>
<node>value</node>
</root>
Here's my pseudo-code:
import xml.
create empty-xml.
foreach child of imported-xml-root-node,
recursively clone node structure without data.
if clone does not match one already in empty-xml,
then add clone to empty-xml.
I'm trying to get a result that looks like this:
<root>
<node/>
<node>
<sub/>
</node>
</root>
Note that my piddly example data is only 3 nodes deep. In production, there will be an unknown number of descendants, so an acceptable answer needs to handle variable node depths.
Failed Approaches
I have reviewed The DOMNode class which has a cloneNode method with a recursive option that I would like to use, although it would take some extra work to purge the data. But while the class contains a hasChildNodes function which returns a boolean, I can't find a way to actually return the collection of children.
$doc = new DOMDocument();
$doc->loadXML($xml);
$root_node = $doc->documentElement;
if ( $root_node->hasChildNodes() ) {
// looking for something like this:
// foreach ($root_node->children() as $child)
// $doppel = $child->cloneNode(true);
}
Secondly, I have tried my hand with the The SimpleXMLElement class which does have an awesome children method. Although it's lacking the recursive option, I built a simple function to surmount that. But the class is missing a clone/copyNode method, and my function is bloating into something nasty to compensate. Now I'm considering combining usage of the two classes so I've got access to both SimpleXMLElement::children and DOMDocument::cloneNode, but I can tell this is not going cleanly and surely this problem can be solved better.
$sxe = new SimpleXMLElement($xml);
$indentation = 0;
function getNamesRecursive( $xml, &$indentation )
{
$indentation++;
foreach($xml->children() as $child) {
for($i=0;$i<$indentation;$i++)
echo "\t";
echo $child->getName() . "\n";
getNamesRecursive($child,$indentation);
}
$indentation--;
}
getNamesRecursive($sxe,$indentation);
Consider XSLT, the special-purpose language designed to transform XML files. And PHP maintains an XSLT 1.0 processor. You simply need to keep items of position 1 and copy only its elements not text.
XSLT (save as .xsl file to use below in php)
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*"/>
<!-- Identity Transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Remove any nodes position greater than 2 -->
<xsl:template match="*[position() > 2]"/>
<!-- Copy only tags -->
<xsl:template match="/*/*/*">
<xsl:copy/>
</xsl:template>
</xsl:transform>
PHP
// LOAD XML AND XSL FILES
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->load('Input.xml');
$xslfile = new DOMDocument('1.0', 'UTF-8');
$xslfile->load('Script.xsl');
// TRANSFORM XML with XSLT
$proc = new XSLTProcessor;
$proc->importStyleSheet($xslfile);
$newXml = $proc->transformToXML($xml);
// ECHO OUTPUT STRING
echo $newXml;
# <root>
# <node/>
# <node>
# <sub/>
# </node>
# </root>
// NEW DOM OBJECT
$final = new DOMDocument('1.0', 'UTF-8');
$final->loadXML($newXml);
well here's my stinky solution. suggestions for improvements or completely new better answers are still very welcome.
$xml = '
<root>
<node/>
<node>
<sub>more</sub>
</node>
<node>
<sub>another</sub>
</node>
<node>value</node>
</root>
';
$doc = new DOMDocument();
$doc->loadXML($xml);
// clone without data
$empty_xml = new DOMDocument();
$empty_xml->appendChild($empty_xml->importNode($doc->documentElement));
function clone_without_data(&$orig, &$clone, &$clonedoc){
foreach ($orig->childNodes as $child){
if(get_class($child) === "DOMElement")
$new_node = $clone->appendChild($clonedoc->importNode($child));
if($child->hasChildNodes())
clone_without_data($child,$new_node,$clonedoc);
}
}
clone_without_data($doc->documentElement, $empty_xml->documentElement, $empty_xml);
// remove all duplicates
$distinct_structure = new DOMDocument();
$distinct_structure->appendChild($distinct_structure->importNode($doc->documentElement));
foreach ($empty_xml->documentElement->childNodes as $child){
$match = false;
foreach ($distinct_structure->documentElement->childNodes as $i => $element){
if ($distinct_structure->saveXML($element) === $empty_xml->saveXML($child)) {
$match = true;
break;
}
}
if (!$match)
$distinct_structure->documentElement->appendChild($distinct_structure->importNode($child,true));
}
$distinct_structure->formatOutput = true;
echo $distinct_structure->saveXML();
Which results in this output:
<?xml version="1.0"?>
<root>
<node/>
<node>
<sub/>
</node>
</root>
I've been battling with this all day :(
Although I found answers for similar questions they don't update an existing XML, they create a new XML.
Any help would be very much appreciated.
This is the XML I'm loading and trying to sort just the images->image nodes:
<?xml version="1.0"?>
<stuff>
<other_nodes>
</other_nodes>
<images>
<image><sorted_number><![CDATA[1]]></sorted_number></image>
<image><sorted_number><![CDATA[3]]></sorted_number></image>
<image><sorted_number><![CDATA[2]]></sorted_number></image>
</images>
</stuff>
//load the xml into a var
$theXML = //load the xml from the database
$imageNode = $theXML->images;
//sort the images into sorted order
$d = $imageNode;
// turn into array
$e = array();
foreach ($d->image as $image) {
$e[] = $image;
}
// sort the array
usort($e, function($a, $b) {
return $a->sorted_number - $b->sorted_number;
});
//now update the xml in the correct order
foreach ($e as $node) {
//???unsure how to update the images node in my XML
}
SimpleXML is too simple for your task. There is no easy way to reorder nodes. Basically, after your sorting routine, you have to reconstruct <image> nodes, but you have CDATA inside, and SimpleXML can't directly add CDATA value.
If you want try by this way, here you can find a cool SimpleXML class extension that add CDATA property, but also this solution use DOMDocument.
Basically, IMHO, since every solution require DOM, the best way is to use directly DOMDocument and — eventually — (re)load XML with SimpleXML after transformation:
$dom = new DOMDocument();
$dom->loadXML( $xml, LIBXML_NOBLANKS );
$dom->formatOutput = True;
$images = $dom->getElementsByTagName( 'image' );
/* This is the same as your array conversion: */
$sorted = iterator_to_array( $images );
/* This is your sorting routine adapted to DOMDocument: */
usort( $sorted, function( $a, $b )
{
return
$a->getElementsByTagName('sorted_number')->item(0)->nodeValue
-
$b->getElementsByTagName('sorted_number')->item(0)->nodeValue;
});
/* This is the core loop to “replace” old nodes: */
foreach( $sorted as $node ) $images->item(0)->parentNode->appendChild( $node );
echo $dom->saveXML();
ideone demo
The main routine add sorted nodes as child to existing <images> node. Please note that there is no need to pre-remove old childs: since we refer to same object, by appending a node in fact we remove it from its previous position.
If you want obtain a SimpleXML object, at the end of above code you can append this line:
$xml = simplexml_load_string( $dom->saveXML() );
Consider an XSLT solution using its <xsl:sort>. As information, XSLT (whose script is a well-formed XML file) is a declarative, special-purpose programming language (same type as SQL), used specifically to manipulate XML documents and sorting is one type of manipulation. Often used as a stylesheet to render XML content into HTML, XSLT is actually a language.
Most general-purpose languages including PHP (xsl extension), Python (lxml module), Java (javax.xml), Perl (libxml), C# (System.Xml), and VB (MSXML) maintain XSLT 1.0 processors. And various external executable processors like Xalan and Saxon (the latter of which can run XSLT 2.0 and recently 3.0) are also available -which of course PHP can call with exec(). Below embeds XSLT as a string variable but can very easily be loaded from an external .xsl or .xslt file.
// Load the XML source and XSLT file
$doc = new DOMDocument();
$doc->loadXML($xml);
$xsl = new DOMDocument;
$xslstr = '<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes"
cdata-section-elements="sorted_number" />
<xsl:strip-space elements="*"/>
<!-- IDENTITY TRANSFORM (COPIES ALL CONTENT AS IS) -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- SORT IMAGE CHILDREN IN EACH IMAGES NODE -->
<xsl:template match="images">
<xsl:copy>
<xsl:apply-templates select="image">
<xsl:sort select="sorted_number" order="ascending" data-type="number"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:transform>';
$xsl->loadXML($xslstr);
// Configure the processor
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXml = $proc->transformToXML($doc);
echo $newXml;
Result (notice <![CData[]]> being preserved)
<?xml version="1.0" encoding="UTF-8"?>
<stuff>
<other_nodes/>
<images>
<image>
<sorted_number><![CDATA[1]]></sorted_number>
</image>
<image>
<sorted_number><![CDATA[2]]></sorted_number>
</image>
<image>
<sorted_number><![CDATA[3]]></sorted_number>
</image>
</images>
</stuff>
Before going deeper, is a save of the sorted state really necessary? Like in a database, you can always sort items when retrieving them, same here with the code you have already written.
That said, "updating" in your case means delete all <image> nodes and add them back in order.
Update:
see fusion3k's answer, that it is not necessary to delete nodes, but just append them. I'd suggest to go with his solution.
You are using SimpleXml, which does not provide methods for copying nodes. You will need to re-create every single node, child-node, attribute.
Your XML looks simple, but I guess it is an example and your real XML is more complex. Then rather use DOM and its importNode() method, which can copy complex nodes, including all their attributes and children.
On the other hand, SimpleXml to me feels much easier, so I combine both:
$xml = simplexml_load_string($x); // assume XML in $x
$images = $xml->xpath("/stuff/images/image");
usort($images, function ($a, $b){
return strnatcmp($a->sorted_number, $b->sorted_number);
});
Comments:
xpath() is a quick way to get all items into an array of SimpleXml objects.
$images is sorted now, but we can't delete the original nodes, because $images holds references to these nodes.
This is why we need to save $images to a new, temporary document.
$tmp = new DOMDocument('1.0', 'utf-8');
$tmp->loadXML("<images />");
// add image to $tmp, then delete it from $xml
foreach($images as $image) {
$node = dom_import_simplexml($image); // make DOM from SimpleXml
$node = $tmp->importNode($node, TRUE); // import and append in $tmp
$tmp->getElementsByTagName("images")->item(0)->appendChild($node);
unset($image[0]); // delete image from $xml
}
Comments:
using DOM now, because I can copy nodes with importNode()
at this point, $tmp has all the <image> nodes in the desired order, $xml has none.
To copy nodes back from $tmp to $xml, we need to import $xml into DOM:
$xml = dom_import_simplexml($xml)->ownerDocument;
foreach($tmp->getElementsByTagName('image') as $image) {
$node = $xml->importNode($image, TRUE);
$xml->getElementsByTagName("images")->item(0)->appendChild($node);
}
// output...
echo $xml->saveXML();
see it in action: https://eval.in/535800