Sort Xml via Attribute Value PHP

Sort XML via attribute value PHP

This should give you what you want:

$string = <<<EOS
<page>
<talentTrees>
<tree name="Football" order="2" />
<tree name="Baseball" order="0" />
<tree name="Frisbee" order="1" />
</talentTrees>
</page>
EOS;

$xml = simplexml_load_string($string);

$trees = $xml->xpath('/page/talentTrees/tree');
function sort_trees($t1, $t2) {
return strcmp($t1['order'], $t2['order']);
}

usort($trees, 'sort_trees');
var_dump($trees);

$trees are now sorted by the order attribute.

How to sort xml child via attribute value PhP

SimpleXML is not good at manipulating nodes so this is a lot easier with DOM.

$document = new DOMDocument();
$document->preserveWhiteSpace = FALSE;
$document->loadXML($xmlString);
$xpath = new DOMXpath($document);

// convert returned node list into an array
$vocabularios = iterator_to_array(
// fetch all vocabulario elements
$xpath->evaluate('/expression/vocabulario')
);
// sort the array
usort(
$vocabularios,
static function($a, $b) {
// compare the word attribute
return strcasecmp($a->getAttribute('word'), $b->getAttribute('word'));
}
);

// iterate the sorted array
foreach ($vocabularios as $vocabulario) {
// append node to the expression document element
// that moves nodes from their current position
$document->documentElement->appendChild($vocabulario);
}

$document->formatOutput = TRUE;
echo $document->saveXML();

To sort by child element text content you would just have to change the compare function.

usort(
$vocabularios,
static function($a, $b) use ($xpath) {
// compare the word child element
return strcasecmp(
$xpath->evaluate('string(word)', $a),
$xpath->evaluate('string(word)', $b)
);
}
);

Sorting XML data based on Attribute Value

What I would do in this case, if I don't have a way to change how the data is being output by the database, I would load it into PHP as an associative array and sort the keys. Something like this:

<?php 

$xmlData = 'http://xml.betfred.com/Horse-Racing-Daily.xml';
$xml = simplexml_load_file($xmlData);
$curdate = date('d/m/Y');

$new_array = array();
$limit = 5;
$c = 0;
foreach ($xml->event as $event) {
if ($limit == $c) {
break;
}
$c++;
$eventd = substr($event->attributes()->{'date'},6,2);
$eventm = substr($event->attributes()->{'date'},4,2);
$eventy = substr($event->attributes()->{'date'},0,4);
$eventt = $event->attributes()->{'time'};
$new_array[$eventy.$eventm.$eventd.$eventt] = array(
'eventd' => $eventd,
'eventm' => $eventm,
'eventy' => $eventy,
'eventt' => $eventt,
'eventdate' => $eventd.'/'.$eventm.'/'.$eventy,
'eventid' => $event->attributes()->{'eventid'},
'eventtime' => $event->attributes()->{'time'},
'eventname' => $event->attributes()->{'name'},
'venue' => $event->attributes()->venue,
);
}

ksort($new_array);
foreach ($new_array as $event_time => $event_data) {
echo "<a href=\"event/".$event_data['eventname']."/".$event_data['eventid']."\">".$event_data['venue'].' - '.$event_data['eventtime'].' - '.$event_data['eventname']."</a><br />".$event_data['eventdate']."<br />";
}

Of course this is NOT TESTED.

EDIT - Tested and it works now.

The point is that you load the array first with the information you need, sort by the key of the associative array and then print out what you want. You could change the limit on either the full loaded list, or just the first 5 from the xml file. Good luck!

PHP: How to sort XML alphabetically by attribute before for each loop

You need to use usort. This should work:

<?php
$url = '001.XML';
$xml = simplexml_load_file($url);

$items = array();
foreach($xml->Images->ImageGalleryEntry as $item) {
$items[] = $item;
};

// Custom sort on the names of the items:
usort ($items, function($a, $b) {
return strcmp($a['Name'], $b['Name']);
});

foreach ($items as $item) {
echo $item['FileName'] . "<br/>" . $item['Name'] . "<br/>" . $item['Description'] . "<br/><br/>";
}

Sort XML-elements by their attributes

If you only want to sort adjacent element elements then I think processing elements with xsl:for-each-group select="*" group-adjacent="boolean(self::element) allows you to identify them and then inside of the for-each-group you can process the groups of element elements sorted based on the attribute and the other elements without sorting:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
exclude-result-prefixes="xs math map array"
version="3.0">

<xsl:mode on-no-match="shallow-copy"/>

<xsl:output indent="yes"/>

<xsl:template match="measure">
<xsl:copy>
<xsl:for-each-group select="*" group-adjacent="boolean(self::element)">
<xsl:choose>
<xsl:when test="current-grouping-key()">
<xsl:apply-templates select="current-group()">
<xsl:sort select="xs:decimal(@n)"/>
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bFN1y9m/

If you want to sort all element child elements and swap them around based on the original position of all element children then I think the following, which first computes the sorted sequence of attribute values and then gives each element to be sorted the position of the original input order, helps:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">

<xsl:mode on-no-match="shallow-copy"/>

<xsl:output indent="yes"/>

<xsl:template match="measure">
<xsl:copy>
<xsl:variable name="original-order" as="xs:string*" select="node()!generate-id()"/>
<xsl:variable name="elems" as="element(element)*" select="element"/>
<xsl:variable name="sort-order" as="xs:decimal*" select="sort(element/xs:decimal(@n))"/>
<xsl:apply-templates>
<xsl:sort
select="if (. instance of element(element))
then
let $sort-pos := index-of($sort-order, xs:decimal(@n)),
$orig-el := $elems[$sort-pos]
return
index-of($original-order, $orig-el!generate-id())
else position()"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bFN1y9m/1

For XSLT 3 processors with higher-order function sort support (e.g. Saxon PE or EE or Altova) I think this can be improved to use a sequence of elements or element ids for the original sort order:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">

<xsl:mode on-no-match="shallow-copy"/>

<xsl:template match="measure">
<xsl:copy>
<xsl:variable name="original-order" as="xs:string*" select="node()!generate-id()"/>
<xsl:variable name="elems" as="element(element)*" select="element"/>
<xsl:variable name="sort-order" as="xs:decimal*" select="sort(element/xs:decimal(@n))"/>
<xsl:apply-templates>
<xsl:sort
select="if (. instance of element(element))
then
let $sort-pos := index-of($sort-order, xs:decimal(@n)),
$orig-el := $elems[$sort-pos]
return
index-of($original-order, $orig-el!generate-id())
else position()"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

That way I think the approach should work even if there are various elements with the same sort key value (e.g. @n value).

Sort XML based on name, attribute name, attribute value and node value

You're currently sorting all attributes of an element by local name and value, and then all children (again by local name and string value).

So far, so good.

One difficulty you face is what exactly you mean by sorting by "attribute names". From your example, it looks as if you want elements sorted by a list of their attribute names in alphabetic order, so that the sort keys for the children of your NodeGroup element are

'NodeC', 'asc name', '2 z', 103
'NodeC', 'name', 'a', 102
'NodeC', 'name', 'b', 201

The next difficulty is that there's no obvious way to obtain the value 'asc name' from an XPath 1.0 expression with the first NodeC of your NodeGroup element as context node. It's possible to generate the string, of course, but it requires a call to a named template. (Or, to be more precise: I don't see how to generate it without such a call.)

XSLT 2.0 solution

The problem is relatively straightforward in XSLT 2.0; the following fragments show the crucial bits:

<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="."/>
</xsl:apply-templates>
<xsl:apply-templates select="node()">
<xsl:sort select="local-name()"/>
<xsl:sort select="string-join(local:key2(.), ' ')"/>
<xsl:sort select="string-join(local:key3(.), ' ')"/>
<xsl:sort select="." data-type="number"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

<xsl:function name="local:key2" as="xs:string*">
<xsl:param name="e" as="node()"/>
<xsl:for-each select="$e/@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="string()"/>
<xsl:value-of select="local-name()"/>
</xsl:for-each>
</xsl:function>

<xsl:function name="local:key3" as="xs:string*">
<xsl:param name="e" as="node()"/>
<xsl:for-each select="$e/@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="string()"/>
<xsl:value-of select="string()"/>
</xsl:for-each>
</xsl:function>

This general approach can also be used in XSLT 1.0 with the EXSLT extension for user-defined functions.

Solution in XSLT 1.0 with EXSLT functions

If your XSLT 1.0 processor supports EXSLT-style user-defined functions, you may be able to do something similar in XSLT 1.0. (My initial attempts failed, but the errors disappeared when I remembered to add the extension-element-prefixes attribute to the stylesheet element.)

<xsl:stylesheet version="1.0"
extension-element-prefixes="func"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:func="http://exslt.org/functions"
xmlns:local="http://example.com/nss/dummy">

<xsl:output encoding="utf-8"
method="xml"
omit-xml-declaration="yes"
indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="."/>
</xsl:apply-templates>
<xsl:apply-templates select="node()">
<xsl:sort select="local-name()"/>
<xsl:sort select="local:key2(.)"/>
<xsl:sort select="local:key3(.)"/>
<xsl:sort select="." data-type="number"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

<func:function name="local:key2">
<xsl:param name="e" select="."/>

<func:result>
<xsl:for-each select="$e/@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="string()"/>
<xsl:value-of select="concat(local-name(), ' ')"/>
</xsl:for-each>
</func:result>
</func:function>

<func:function name="local:key3">
<xsl:param name="e" select="."/>
<func:result>
<xsl:for-each select="$e/@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="string()"/>
<xsl:value-of select="concat(string(), ' ')"/>
</xsl:for-each>
</func:result>
</func:function>
</xsl:stylesheet>

When run on your input with xsltproc, this produces the desired output.

You might also be able to do something clever in XSLT 1.0 with the node-set extension.

Two-stage pipeline in unextended XSLT 1.0

But the simplest way I can see to solve this problem in unextended XSLT 1.0 is to pipeline two stylesheets together. The first one adds two attributes to every element, to provide sort keys 2 and 3. (Adjust the named templates to make them do what you want.)

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:PJ="http://example.com/PankajJaju">
<xsl:output encoding="utf-8"
method="xml"
omit-xml-declaration="yes"
indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:if test="self::*">
<xsl:attribute name="PJ:attribute-names"
namespace="http://example.com/PankajJaju">
<xsl:call-template name="attribute-name-list"/>
</xsl:attribute>
<xsl:attribute name="PJ:attribute-values"
namespace="http://example.com/PankajJaju">
<xsl:call-template name="attribute-value-list"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>

<xsl:template name="attribute-name-list">
<xsl:for-each select="@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="string()"/>
<xsl:value-of select="concat(local-name(), ' ')"/>
</xsl:for-each>
</xsl:template>
<xsl:template name="attribute-value-list">
<xsl:for-each select="@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="string()"/>
<xsl:value-of select="concat(string(), ' ')"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

I've put them into a namespace to reduce the likelihood of name collisions.

The second one uses the sort keys to perform the actual sort and suppresses the temporary attributes.

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:PJ="http://example.com/PankajJaju">
<xsl:output encoding="utf-8"
method="xml"
omit-xml-declaration="yes"
indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*">
<xsl:sort select="local-name()"/>
<xsl:sort select="."/>
</xsl:apply-templates>
<xsl:apply-templates select="node()">
<xsl:sort select="local-name()"/>
<xsl:sort select="@PJ:attribute-names"/>
<xsl:sort select="@PJ:attribute-values"/>
<xsl:sort select="."/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

<xsl:template match="@PJ:attribute-names | @PJ:attribute-values"/>
</xsl:stylesheet>

These can be pipelined together using whatever technology you prefer. Using xsltproc from the bash command line, for example, and assigning the names p1.xsl and p2.xsl to pipeline stylesheets 1 and 2 ...

xsltproc p1.xsl input.xml | xsltproc p2.xsl -

This produces the output you say you want.

How to sort a xml file using DOM

The answer you cite is for SimpleXML, but you are using DOMDocument.

If you want to continue to use DOMDocument you need to keep its API in mind.

$dom = new DOMDocument();
$dom->load('DOM.xml');
$xp = new DOMXPath($dom);

$booklist = $xp->query('/library/book');

// Books is a DOMNodeList, not an array.
// This is the reason for your usort() warning.

// Copies DOMNode elements in the DOMNodeList to an array.
$books = iterator_to_array($booklist);

// Second, your sorting function is using the wrong API
// $node['id'] is SimpleXML syntax for attribute access.
// DOMElement uses $node->getAttribute('id');
function sort_by_numeric_id_attr($a, $b)
{
return (int) $a->getAttribute('id') - (int) $b->getAttribute('id');
}

// Now usort()
usort($books, 'sort_by_numeric_id_attr');

// verify:
foreach ($books as $book) {
echo $book->C14N(), "\n";
}

If you need to create a new output document with the nodes sorted, create a new document, import the root element, then import the book nodes in sorted order and add to the document.

$newdoc = new DOMDocument('1.0', 'UTF-8');
$libraries = $newdoc->appendChild($newdoc->importNode($dom->documentElement));
foreach ($books as $book) {
$libraries->appendChild($newdoc->importNode($book, true));
}

echo $newdoc->saveXML();

However, a much better approach is to use XSLT:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- file "sort_by_numeric_id.xsl" -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" method="xml" />

<xsl:template match="node()|@*">
<xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
</xsl:template>

<xsl:template match="/*">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="*">
<xsl:sort select="@id" data-type="number"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

Then use XSLTProcessor (or xsltproc from the command line):

$xsltdoc = new DOMDocument();
$xsltdoc->load('sort_by_numeric_id.xsl');

$xslt = new XSLTProcessor();
$xslt->importStyleSheet($xsltdoc);

// You can now use $xslt->transformTo*() methods over and over on whatever documents you want

$libraryfiles = array('library1.xml', 'library2.xml');

foreach ($libraryfiles as $lf) {
$doc = new DOMDocument();
$doc->load($lf);

// write the new document
$xslt->transformToUri($doc, 'file://'.preg_replace('/(\.[^.]+)?$/', '-sorted$0', $lf, 1);

unset($doc); // just to save memory
}

How to sort xml folders and files by their attribute value using xsl?

You just need to add a xsl:sort instruction to the famous identity transform template:

<?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" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()">
<xsl:sort select="@order" data-type="number" order="ascending"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Note that your XML example is not a well-formed document: you are missing a root element.



Related Topics



Leave a reply



Submit