crooksy88
crooksy88

Reputation: 3851

PHP sort and update nodes

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

}

Upvotes: 1

Views: 1244

Answers (3)

fusion3k
fusion3k

Reputation: 11689

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

Upvotes: 3

Parfait
Parfait

Reputation: 107587

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>

Upvotes: 1

michi
michi

Reputation: 6625

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

Upvotes: 0

Related Questions