views:

686

answers:

5

I've read what I've found on Stackoverflow and am still unclear on this.

I have an array of SimpleXML objects something like this:

array(2) {
  [0]=>
  object(SimpleXMLElement)#2 (2) {
    ["name"]=>
    string(15) "Andrew"
    ["age"]=>
    string(2) "21"
  }
  [1]=>
  object(SimpleXMLElement)#3 (2) {
    ["name"]=>
    string(12) "Beth"
    ["age"]=>
    string(2) "56"
  }
}

And I want to be able to sort by whatever column, ascending or descending. Something like:

sort($data, 'name', 'asc');

Where I can pass in the above array of objects and sort by the value of whichever key I like.

For reference, a similar .NET solution would be along these lines:

XmlSortOrder order = XmlSortOrder.Ascending;
    if ( sortDirection == "asc" ) {
        order = XmlSortOrder.Ascending;
    }
    expression.AddSort( columnSortingOn + "/text()", order, 
        XmlCaseOrder.UpperFirst, "en-us", XmlDataType.Text ); 

I've seen people say

"Use usort"

Followed by a basic example from the PHP manual but this doesn't really explain it. At least not to me. I've also seen people suggest using an external library like SimpleDOM but I want to avoid using something external for this (seemingly, though I cannot yet solve it) little thing.

Any help is appreciated, Thanks!

+1  A: 

I was ready to recommend usort() until I realized you already beat me to it. Since code examples haven't done much good in the past, I'll try to just explain it in plain English and hopefully that'll get you pointed in the right direction.

By using usort(), you create your own user-generated "algorithm". The usort() function calls your own comparison function to determine how each of your objects relates to one another. When writing your comparison function, you will get passed in two objects within your array. With those two objects, you return a result that essentially tells usort() whether the first object is LESS THAN, EQUAL TO, or GREATER THAN the second object. You do this by returning -1, 0, or 1 (respectively). That's it. You only have to concern yourself with how two objects compare to each other and the actual sorting mechanics are handled by usort() for you.

Ok, now for a semi-practical example:

function myCompare($obj1, $obj2) {
    if($obj1->someInt == $obj2->someInt) {
        return 0; // equal to
    } else if($obj1->someInt < $obj2->someInt) {
        return -1; // less than
    } else {
        return 1; // greater than
    }
}

$myArray = {a collection of your objects};

usort($myArray, 'myCompare');

This is pretty much the example in the PHP Manual, but hopefully it makes sense in context now. Let me know if I'm not being clear on something.

bkuhns
the only problem with this is he needs to make explicit functions for each property because he wants to be able to sort them by any possible xml element value. Therefore he needs to come up with an intermediary that will make it possible to use a supplied property and order at call time.
prodigitalson
You can use a global variable to specify which column will be used in the comparison (keep the data types in mind to compare between strings and numbers). I'd never actually recommend someone use globals though, so more ideally, implement this in a class and use a member variable to specify the column. I've never used `usort()` this way though, so I can't promise it'll work. I would run a quick test if I had more time.
bkuhns
@bkuhns:Id say wrapping it up in a class is the ideal intermediary. Perhaps by extending ArrayObject.
prodigitalson
+2  A: 

The usort function allows to you tell PHP

Hey, you! Sort this array I'm giving you with this function I wrote.

It has nothing specifically to do with SimpleXML. It's a generic function for sorting PHP built-in array data collection.

You need to write a function, instance method, or static method to sort the array. The second argument to usort accepts a PHP Callback, which is a pseudo-type that lets you specify which function, instance method, or static method.

The function you write will accept two arguments. These will be two different values from your array

function cmp($a, $b)
{
            if ($a == $b) {
                return 0;
            }
            if($a < $b) {
                return -1;
            }
            if($a > $b) {
                return 1;
            }
}

You need to write this function to return one of three values.

If $a == $b, return 0
If $a > $b, return -1
if $a > $v, return 1  

When you call usort, PHP will run through your array, calling your sorting function/method (in this case cmp over and over again until the array is sorted. In your example, $a and $b will be SimpleXML objects.

Alan Storm
+1  A: 

Here's another example of using usort(). This one allows you to specify the object variable and the sort direction:

function sort_obj_arr(& $arr, $sort_field, $sort_direction)
{
    $sort_func = function($obj_1, $obj_2) use ($sort_field, $sort_direction)
    {
        if ($sort_direction == SORT_ASC) {
            return strnatcasecmp($obj_1->$sort_field, $obj_2->$sort_field);
        } else {
            return strnatcasecmp($obj_2->$sort_field, $obj_1->$sort_field);
        }
    };
    usort($arr, $sort_func);
}

Test code;

class TestClass
{
    public $name;
    public $age;

    public function __construct($name, $age)
    {
        $this->name = $name;
        $this->age  = $age;
    }
}

$test[] = new TestClass('Tom', 28);
$test[] = new TestClass('Mary', 48);
$test[] = new TestClass('Beth', 38);
$test[] = new TestClass('Cindy', 18);
$test[] = new TestClass('Sid', 58);
$test[] = new TestClass('Mandy', 8);

$field = 'age';
$direction = SORT_DESC;
sort_obj_arr($test, $field, $direction);

echo '<pre>';
print_r($test);
echo '</pre>';
GZipp
+1  A: 

I guess the people suggesting to use SimpleDOM would be me. :)

I've written SimpleDOM::sort() exactly for that situation, because in order to sort SimpleXMLElements by an arbitration expression (or arbitrary expressions) you need to use array_multisort() which is boring and won't teach you anything useful.

Here's the short version of how it works: first you create a proxy array of key=>value pairs corresponding to each SimpleXMLElement and the value with which they'll be sorted. In your example, if you want to sort them by <age/>, the array would be array(21, 56). Then you call array_multisort() with the "proxy array" as first argument, followed by any number of sorting modifiers such as SORT_DESC or SORT_NUMERIC then finally the array you want to sort, which will be passed by reference.

You will end up with something like that:

$nodes = array(
    new SimpleXMLElement('<person><name>Andrew</name><age>21</age></person>'),
    new SimpleXMLElement('<person><name>Beth</name><age>56</age></person>')
);

function xsort(&$nodes, $child_name, $order = SORT_ASC)
{
    $sort_proxy = array();

    foreach ($nodes as $k => $node)
    {
        $sort_proxy[$k] = (string) $node->$child_name;
    }

    array_multisort($sort_proxy, $order, $nodes);
}

xsort($nodes, 'name', SORT_ASC);
print_r($nodes);

xsort($nodes, 'age', SORT_DESC);
print_r($nodes);

But really, instead of burdening yourself with more code you'll have to maintain and possibly ending up rewriting array_multisort() in userspace, you should leverage existing solutions. There's nothing interesting in such a sorting algorithm/routine, your time would be better spent on something that doesn't already exist.

Josh Davis
Well heck Josh, you are persistent! I suppose I will give it a try as it would allow me to move on to the next stumbling block and I could probably use it in other places on this project.
gaoshan88
I'm not presistent, only consistent in advocating for code reuse. If the snippet I posted above works for you then by all mean please use that. Or use closures if you prefer, whatever works for you. As you said, your goal is to find a solution that will allow you to move on to the next stumbling block and won't require you to look back later.
Josh Davis
That "persistent" was meant to be complimentary. I'm sold on SimpleDOM. It just works. Fast. Great stuff, Josh.
gaoshan88
Oh thank you :) Don't worry I didn't take offence
Josh Davis
@gaoshan88 - Glad you got a solution. Have any of the answers explaining and/or demonstrating usort() been good enough to help you understand it? (That along with not using an external library seemed to be a big part of your question.)
GZipp
@GZipp - Yes, most of the comments here contained some useful bits, yours included (which I voted up). The reason I choose Josh's is that he not only made a good argument for using his library, he included a stand alone solution that works.
gaoshan88
A: 

Josh Davis' solution doesn't seem to work when loading an XML file via:

$nodes = simplexml_load_file('/home/usr/public_html/feeds/deadlines.php');

I get the following error:

Warning: array_multisort() [function.array-multisort]: Argument #3 is expected to be an array or a sort flag in /home/usr/public_html/feeds/deadlines.php on line 8

It relates to: array_multisort($sort_proxy, $order, $nodes);

Dr. DOT
xpath is necessary when loading XML via simplexml_load_file(). See http://stackoverflow.com/questions/3998722/how-to-sort-a-multi-dimensional-xml-file
Dr. DOT