Sorting an array of SimpleXML objects - php

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!

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.

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.

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.

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>';

it's a old thread but here is my solution that a use to extract information from a rss feed in order to sort by title
$xml = simplexml_load_file('rss.xml');
$msg = array();
$msg_count = $xml->channel->item->count();
for ($x=0;$x<$msg_count;$x++){
$msg[$x]['titl'] = (string)$xml->channel->item[$x]->title;
$msg[$x]['link'] = (string)$xml->channel->item[$x]->link;
$msg[$x]['date'] = (string)$xml->channel->item[$x]->pubDate;
$msg[$x]['time'] = strtotime(substr((string)$xml->channel->item[$x]->pubDate,4));
$msg[$x]['desc'] = (string)$xml->channel->item[$x]->description;
$msg[$x]['auth'] = (string)$xml->channel->item[$x]->author;
}
foreach ($msg as $key => $row) {
$titl[$key] = $row['titl'];
$link[$key] = $row['link'];
$pdat[$key] = $row['date'];
$time[$key] = $row['time'];
$cate[$key] = $row['cate'];
$desc[$key] = $row['desc'];
$auth[$key] = $row['auth'];
}
array_multisort(
//change $titl to any variable created by "foreach"
$titl, SORT_ASC,SORT_NATURAL | SORT_FLAG_CASE,
$msg);

Related

Order alphabetically SimpleXML array by attribute

I need a bit of help ordering a SimpleXML array in an alphabetical order using one of it's attribute ($url). I have the following code that generate a page:
<?php
$xml = simplexml_load_file($xml_url);
$ids = array();
foreach($xml->list as $id) {
$ids[] = $id;
}
//It works but randomize instead of sorting alphabetically
usort($ids, function ($a, $b) {
return strnatcmp($a['url'], $b['url']);
});
foreach ($ids as $id) {
PopulatePage($id);
}
function PopulatePage($id) {
$url = $id->url;
$img = $id->img;
//OTHER CODES TO GENERATE THE PAGE
}?>
QUESTION RESOLVED!
There is no conversion needed, you already have an array which you can sort, you just need to understand how usort callbacks work. Each of the items in your $ids array is a SimpleXMLElement object, and each time your callback is run, it will be given two of those objects to compare. Those objects will be exactly the same as in the existing PopulatePage function, so accessing the URL needs to happen exactly as it does there ($url = $id->url;) not using a different notation ($id['url']).
To make it more explicit, let's write a named function with lots of clear variable names:
function CompareTwoIds(SimpleXMLElement $left_id, SimpleXMLElement $right_id): int {
$left_url = $left_id->url;
$right_url = $right_id->url;
return strncatcmp($left_url, $right_url);
}
Now you can test calling that function manually, and use it as the callback when you're happy:
usort($ids, 'CompareTwoIds');
Once you're comfortable with the principles, you may decide to skip the extra verbosity and just write this, which is completely equivalent:
usort($ids, fn($a,$b) => strncatcmp($a->url, $b->url));

PHP Sorting String Array With Substring As Identifier

I want to sort an array of strings where the substring I want to use as sorting identifier is in the middle of the string in question. I've already tried to do that using a very crude technique and I was wondering if there was a better way to do this.
Here is what I've come up with so far:
<?php
$tracks = array(
"Arcane Library_Track2.mp3",
"Deeper Ocean_Track4.mp3"
"Neon Illumination_Track3.mp3",
"Sound Of Cars_Track1.mp3",
);
$tracksreversed = array();
$sortedtracks = array();
foreach( $tracks as $t) {
array_push( $tracksreversed, strrev($t) );
}
sort($tracksreversed);
foreach( $tracksreversed as $t ) {
array_push( $sortedtracks, strrev($t) );
}
$tracks = $sortedtracks;
?>
This method works but there are two issues. For one it uses two loops and the second problem is that this method will fail as soon as we include more file types like ogg or wav.
Is there a way to easily sort this string according to the _Track1, _Track2, _Track3 indentifiers? Maybe some regex sorting could be done here or perhaps there is some other method I could do which makes this more compact?
Maybe the usort function will be useful. Like:
$getIdx = function(string $name) {
preg_match('/_Track(?<idx>\d+)/', $name, $matches);
return $matches['idx'];
};
usort($tracks, function ($a, $b) use ($getIdx) {
return $getIdx($a) <=> $getIdx($b);
});
Seems to work https://3v4l.org/j65rd

When is it appropriate to use an ArrayObject instead of an Array

I'm wondering when, if ever it would be appropriate to use an ArrayObject() instead of an Array()? Here's an example i've been working on.
To me i'm thinking a simple array will work, but i found ArrayObject() in the manual and I'm wondering, if it would be better to use one instead of a simple array.
public function calculateTotal(){
if(count($this->items) > 0){
$n = 0;
foreach($this->items as $item){
if($item->size == 'small'){
$k = $item->price->small;
}
if($item->size == 'large'){
$k = $item->price->large;
}
$n += $k * $item->quantity;
}
}else{
$n = 0;
}
return (int) $n;
}
Now I'm confused as to how i should construct the object.
for instance can i construct it with the short array syntax?
$this->items = []; //empty object
or should i construct is as an Array object
$this->items = new ArrayObject(); //empty object
I'm also confused as to how exactly i should push new items to the array.
i have the following function i'm writing:
Also how should i append arrray objects to this object?
is this good?
public function additem($item){
$add = [
'item_id'=>$this->item_id(),
'name'=>$item['name'],
'size',$item['size'],
'quantity'=>$item['quantity'],
'price'=>[
'large'=>$item['price'],
'small'=>$item['price']
]
]
array_push($this->items,$add);
}
or should i instead use ArrayObject::append() or some other method?
I checked the manual and it says this:
public void ArrayObject::append ( mixed $value )
Appends a new value as the last element.
Note:
This method cannot be called when the ArrayObject was constructed from an object. Use ArrayObject::offsetSet() instead.
source http://php.net/manual/en/arrayobject.append.php
The reason i'm asking this now is, later on when needing to delete items from this list how will i find what i'm looking for? can i use in_array() on this object.
I apologize in advance for these questions that might seem dumb to you, but keep in mind I'm still learning some of the more technical things. Thank you
There's nothing in your first snippet that would require ArrayObject. KISS and use simple arrays: array_push($this->items,$add); or $this->items []= $add; are both fine.
As a side note, there's a discrepancy between calculateTotal and add in your code: you have to decide whether you want your item structures to be arrays ($item['price']) or objects ($item->price). My advice is to use arrays, but that's really up to you.

How to convert an array of arrays or objects to an associative array?

I'm used to perl's map() function where the callback can assign both the key and the value, thus creating an associative array where the input was a flat array. I'm aware of array_fill_keys() which can be useful if all you want to do is create a dictionary-style hash, but what if you don't necessarily want all values to be the same? Obviously all things can be done with foreach iteration, but what other (possibly more elegant) methods exist?
Edit: adding an example to clarify the transformation. Please don't get hung up on the transformation, the question is about transforming a flat list to a hash where we can't assume that all the values will be the same.
$original_array: ('a', 'b', 'c', 'd')
$new_hash: ('a'=>'yes', 'b'=>'no', 'c'=>'yes', 'd'=>'no')
*note: the values in this example are arbitrary, governed by some business logic that is not really relevant to this question. For example, perhaps it's based on the even-oddness of the ordinal value of the key
Real-world Example
So, using an answer that was provided here, here is how you could parse through the $_POST to get a list of only those input fields that match a given criteria. This could be useful, for example, if you have a lot of input fields in your form, but a certain group of them must be processed together.
In this case I have a number of input fields that represent mappings to a database. Each of the input fields looks like this:
<input name="field-user_email" value="2" /> where each of this type of field is prefixed with "field-".
what we want to do is, first, get a list of only those input fields who actually start with "field-", then we want to create an associative array called $mapped_fields that has the extracted field name as the key and the actual input field's value as the value.
$mapped_fields = array_reduce( preg_grep( '/field-.+/', array_keys( $_POST ) ), function( $hash, $field ){ $hash[substr( $field, 6 )] = $_POST[$field]; return $hash; } );
Which outputs:
Array ( [date_of_birth] => 1 [user_email] => 2 [last_name] => 3 [first_name] => 4 [current_position] => 6 )
(So, just to forestall the naysayers, let me agree that this bit of compact code is arguably a lot less readable that a simple loop that iterates through $_POST and, for each key, checks to see if it has the prefix, and if so, pops it and its value onto an array)
I had the exact same problem some days ago. It is not possible using array_map, but array_reduce does the trick.
$arr = array('a','b','c','d');
$assoc_arr = array_reduce($arr, function ($result, $item) {
$result[$item] = (($item == 'a') || ($item == 'c')) ? 'yes' : 'no';
return $result;
}, array());
var_dump($assoc_arr);
result:
array(4) { ["a"]=> string(3) "yes" ["b"]=> string(2) "no" ["c"]=> string(3) "yes" ["d"]=> string(2) "no" }
As far as I know, it is completely impossible in one expression, so you may as well use a foreach loop, à la
$new_hash = array();
foreach($original_array as $item) {
$new_hash[$item] = 'something';
}
If you need it in one expression, go ahead and make a function:
function array_map_keys($callback, $array) {
$result = array();
foreach($array as $item) {
$r = $callback($item);
$result[$r[0]] = $r[1];
}
return $result;
}
This is a clarification on my comment in the accepted method. Hopefully easier to read. This is from a WordPress class, thus the $wpdb reference to write data:
class SLPlus_Locations {
private $dbFields = array('name','address','city');
public function MakePersistent() {
global $wpdb;
$dataArray = array_reduce($this->dbFields,array($this,'mapPropertyToField'));
$wpdb->insert('wp_store_locator',$dataArray);
}
private function mapPropertyToField($result,$property) {
$result[$property] = $this->$property;
return $result;
}
}
Obviously there is a bit more to the complete solution, but the parts relevant to array_reduce() are present. Easier to read and more elegant than a foreach or forcing the issue through array_map() plus a custom insert statement.
Nice!
A good use case of yield operator!
$arr = array('a','b','c','d');
$fct = function(array $items) {
foreach($items as $letter)
{
yield sprintf("key-%s",
$letter
) => "yes";
}
};
$newArr = iterator_to_array($fct($arr));
which gives:
array(4) {
'key-a' =>
string(3) "yes"
'key-b' =>
string(3) "yes"
'key-c' =>
string(3) "yes"
'key-d' =>
string(3) "yes"
}

detecting infinite array recursion in PHP?

i've just reworked my recursion detection algorithm in my pet project dump_r()
https://github.com/leeoniya/dump_r.php
detecting object recursion is not too difficult - you use spl_object_hash() to get the unique internal id of the object instance, store it in a dict and compare against it while dumping other nodes.
for array recursion detection, i'm a bit puzzled, i have not found anything helpful. php itself is able to identify recursion, though it seems to do it one cycle too late. EDIT: nvm, it occurs where it needs to :)
$arr = array();
$arr[] = array(&$arr);
print_r($arr);
does it have to resort to keeping track of everything in the recursion stack and do shallow comparisons against every other array element?
any help would be appreciated,
thanks!
Because of PHP's call-by-value mechanism, the only solution I see here is to iterate the array by reference, and set an arbitrary value in it, which you later check if it exists to find out if you were there before:
function iterate_array(&$arr){
if(!is_array($arr)){
print $arr;
return;
}
// if this key is present, it means you already walked this array
if(isset($arr['__been_here'])){
print 'RECURSION';
return;
}
$arr['__been_here'] = true;
foreach($arr as $key => &$value){
// print your values here, or do your stuff
if($key !== '__been_here'){
if(is_array($value)){
iterate_array($value);
}
print $value;
}
}
// you need to unset it when done because you're working with a reference...
unset($arr['__been_here']);
}
You could wrap this function into another function that accepts values instead of references, but then you would get the RECURSION notice from the 2nd level on. I think print_r does the same too.
Someone will correct me if I am wrong, but PHP is actually detecting recursion at the right moment. Your assignation simply creates the additional cycle. The example should be:
$arr = array();
$arr = array(&$arr);
Which will result in
array(1) { [0]=> &array(1) { [0]=> *RECURSION* } }
As expected.
Well, I got a bit curious myself how to detect recursion and I started to Google. I found this article http://noteslog.com/post/detecting-recursive-dependencies-in-php-composite-values/ and this solution:
function hasRecursiveDependency($value)
{
//if PHP detects recursion in a $value, then a printed $value
//will contain at least one match for the pattern /\*RECURSION\*/
$printed = print_r($value, true);
$recursionMetaUser = preg_match_all('#\*RECURSION\*#', $printed, $matches);
if ($recursionMetaUser == 0)
{
return false;
}
//if PHP detects recursion in a $value, then a serialized $value
//will contain matches for the pattern /\*RECURSION\*/ never because
//of metadata of the serialized $value, but only because of user data
$serialized = serialize($value);
$recursionUser = preg_match_all('#\*RECURSION\*#', $serialized, $matches);
//all the matches that are user data instead of metadata of the
//printed $value must be ignored
$result = $recursionMetaUser > $recursionUser;
return $result;
}

Categories