How to extract part of multidimensional array tree with PHP - php

I have a huge dynamically generated tree. The tree is generated from a flat array based on each elements "parent_id" attribute.
The final result would for example look like this:
Array
(
[0] => Array
(
[id] => 70
[name] => Top Corp
[parent_id] => 0
[children] => Array
(
[0] => Array
(
[id] => 43
[name] => Department
[parent_id] => 70
[children] => Array
(
[0] => Array
(
[id] => 45
[name] => Building
[parent_id] => 43
[children] => Array
(
[0] => Array
(
[id] => 75
[name] => Office
[parent_id] => 45
)
)
)
How do I extract just a part of the array tree? What functions or methods should I look at?
E.g. how do I say another sub-level (potentially 20-30 levels deep) is now the top.
For example, a pseudo function of sliceTree(45) should produce the following result, aka start the tree from id 45
[0] => Array
(
[id] => 45
[name] => Building
[parent_id] => 43
[children] => Array
(
[0] => Array
(
[id] => 75
[name] => Office
[parent_id] => 45
)
)
)
There is no way to know how deep the tree can go so it a solution needs to be recursive.
I have tried looping the array, looking for the starting id, but I am unsure of how to continue the execution after the point has been found.
My proposed solution is as follows
function sliceTree($tree, $id){
$ret = [];
foreach ($tree as $out) {
// if the top level matches
if($out["id"] == $id){
array_push($ret, $out);
}
else {
if(isset($out["children"])){
foreach ($out["children"] as $c) {
if($c["id"] == $id){
array_push($ret, $c);
}
// probably needs to call itself here
}
}
}
}
return $ret;
}
Which works, but only for top-level elements. How can I make recursive and account for the many levels of children?

The sliceTree() function basically looks for a certain id and returns it. Something like this:
function sliceTree($tree, $branchId)
{
// check all branches
foreach ($tree as $branch) {
// have we found the correct branch?
if ($branch['id'] == $branchId) return $branch;
// check the children
if (isset($branch['children'])) {
$slice = sliceTree($branch['children'], $branchId);
if (isset($slice)) return $slice;
}
}
// nothing was found
return null;
}
As you can see, this routine is recursive. Code is untested.
I'm sorry about the mixed metaphors: branches and children, but you started it.
This function is slightly more complex, than I would like it to be, because in your example the children key is absent when there are no children. I would normally expect it to be there and the value to be an empty array.

Related

Sorting an StdClass Object Array by the values of another array

I have an StdClass Array. Each object is ordered like this (there are about 50 objects in the array, but I thought I'd just put one to show the structure):
stdClass Object (
[display_name] => Bob Simons
[post_title] => Lesson 1
[meta_value] => 100
[comment_approved] => passed
[c_num2] => 26
[term_id] => 3
)
I have another array that looks like this:
Array (
[0] => 3
[1] => 4
[2] => 5
[3] => 16
[4] => 17
[5] => 18
[6] => 19
[7] => 20
[8] => 21
)
The second array defines the sorting of the first one based on the [term_id] field. So essentially, everything with the [term_id] 3 should be at the top of the array, everything with the [term_id] 4 should be next, all based on that second array.
I am trying desperately to figure out how to do this, I've been looking at usort and the like but at a total loss.
I hope someone can help and will be really grateful.
Try this
$sortedArray = [];
foreach ($order as $termId) {
foreach ($objects as $object) {
if ($object->term_id == $termId) {
$sortedArray[] = $object;
}
}
}
$objects holds your stdClass instances and $order your list with the ordered term ids.
In the case the term_id should always order in an ascending way, you can use usort:
usort($objects, function ($obj1, $obj2) {
if ($obj1->term_id == $obj2->term_id) {
return 0;
}
return ($obj1->term_id < $obj2->term_id) ? -1 : 1;
});

Iterate through multidimensional PHP array and output values

I'm having a real headache trying to iterate through an array and output elements. Using the array structure below I want to be able to output each instance of partname.
The following loop outputs the first instance of partname. I can't seem to adapt it to loop through all instances within the array. I'm sure I'm missing something basic.
foreach($ItemsArray['assignments'] as $item) {
$partname = $item['grades'][0]['partname'];
}
Array
(
[assignments] => Array
(
[0] => Array
(
[assigntmentid] => 5101
[grades] => Array
(
[0] => Array
(
[id] => 5101
[name] => Advanced AutoCad
[partid] => 6601
[partname] => Draft
[userid] => 82069
[grade] => 53
[courseid] => 6265
[fullname] => Computer Aided Design
)
)
)
[1] => Array
(
[assigntmentid] => 5101
[grades] => Array
(
[0] => Array
(
[id] => 5101
[name] => Advanced AutoCad
[partid] => 6602
[partname] => Final
[userid] => 82069
[grade] => 35
[courseid] => 6265
[fullname] => Computer Aided Design
)
)
)
)
)
Instead of just coding by slapping the keyboard. Write down what your function needs to do. In english (or whatever language you prefer). This would be something like:
Foreach assignment, loop over all grades and store the partname of
that grade into an array.
And then code it:
function getPartnames($assignments) {
$partNames = array();
foreach ($assignments as $assignment) {
foreach($assignment['grades'] as $grade) {
$partNames[] = $grade['partname'];
}
}
return $partNames;
}
So what did I do? I simply translated english to code.
Some few more tips: Use variables names that make sense. $item; $ItemArray; ... don't make sense. They tell me nothing
use an extra foreach in your loop:
foreach($ItemsArray['assignments'] as $item) {
foreach($item['grades'] as $grade) {
echo $grade['partname'];
}
}

How to count multidimensional array values?

I am building an MLM software in PHP, one of its function is to count the downline performance.
It creates an array from parent-child relationships, which can be seen below.
How can I get the performance (array key: points) of my children, and their grandchildren as well through the fifth down level?
Array
(
[children] => Array
(
[0] => Array
(
[id] => 1
[name] => Medvedev
[email] =>
[points] => 7
[children] => Array
(
[0] => Array
(
[id] => 3
[name] => Putin
[email] =>
[points] => 4
[children] => Array
(
[0] => Array
(
[id] => 5
[name] => Nathan
[email] =>
[points] => 3
)
[1] => Array
(
[id] => 7
[name] => George
[email] =>
[points] => 666
)
)
)
[1] => Array
(
[id] => 4
[name] => Lucas
[email] =>
[points] => 43
)
)
)
)
[id] => 27
[name] => Boss
[email] =>
[points] => 99999
)
this should work with unlimited depth starting from a main array like
$array = array(
'children' => array( /* ADD HERE INFINITE COMBINATION OF CHILDREN ARRAY */ ),
'id' => #,
'name' => '',
'email' => '',
'points' => #
);
function recursive_children_points($arr) {
global $hold_points;
if (isset($arr['points'])) {
$hold_points[] = $arr['points'];
}
if (isset($arr['children'])) {
foreach ($arr['children'] as $children => $child) {
recursive_children_points($child);
}
}
return $hold_points;
}
$points = recursive_children_points($array);
print "<pre>";
print_r($points);
/**
// OUTPUT
Array
(
[0] => 99999
[1] => 7
[2] => 4
[3] => 3
[4] => 666
[5] => 43
)
**/
print "<pre>";
In my opinion this calls for recursion because you'll do the same thing over each flat level of the array: add the points.
so you'll need to
loop over each array
add points found
if we find any children, do it again but with children array
while keeping track of levels and jumping out if you reach your limit
With that in mind I thought of the following solution:
<?php
$values = array();
//first
$values[] = array('name'=>'andrej','points'=>1,'children'=>array());
$values[] = array('name'=>'peter','points'=>2,'children'=>array());
$values[] = array('name'=>'mark','points'=>3,'children'=>array());
//second
$values[0]['children'][] = array('name'=>'Sarah','points'=>4,'children'=>array());
$values[2]['children'][] = array('name'=>'Mike','points'=>5,'children'=>array());
//third
$values[0]['children'][0]['children'][] = array('name'=>'Ron','points'=>6,'children'=>array());
//fourth
$values[0]['children'][0]['children'][0]['children'][] = array('name'=>'Ronny','points'=>7,'children'=>array());
//fifth
$values[0]['children'][0]['children'][0]['children'][0]['children'][] = array('name'=>'Marina','points'=>10,'children'=>array());
//sixth
$values[0]['children'][0]['children'][0]['children'][0]['children'][0]['children'][] = array('name'=>'Pjotr','points'=>400,'children'=>array());
function collect_elements($base, $maxLevel,$child='children',$gather='points', &$catch = array(), $level = 0) {
/* I pass $catch by reference so that all recursive calls add to the same array
obviously you could use it straight away but I return the created array as well
because I find it to be cleaner in PHP (by reference is rare and can lead to confusion)
$base = array it works on
$maxLevel = how deep the recursion goes
$child = key of the element where you hold your possible childnodes
$gather = key of the element that has to be collected
*/
$level++;
if($level > $maxLevel) return; // we're too deep, bail out
foreach ($base as $key => $elem) {
// collect the element if available
if(isset($elem[$gather])) $catch[] = $elem[$gather];
/*
does this element's container have children?
[$child] needs to be set, [$child] needs to be an array, [$child] needs to have elements itself
*/
if (isset($elem[$child]) && is_array($elem[$child]) && count($elem[$child])){
// if we can find another array 1 level down, recurse that as well
collect_elements($elem[$child],$maxLevel,$child,$gather, $catch,$level);
}
}
return $catch;
}
print array_sum(collect_elements($values,5)) . PHP_EOL;
collect_elements will collect the element you're interested in (until maximum depth is reached) and append it to a flat array, so that you can act on it upon return. In this case we do an array_sum to get the total of poins collected
Only the first for parameters are interesting:
collect_elements($base, $maxLevel,$child='children',$gather='points'
not optional:
$base is the array to work on
$maxLevel is the maximum depth the function needs to descend into the arrays
optional:
$child defines the key of the element that contains the children of current element (array)
$gather defines the key of the element that contains what we want to gather
The remaining parameters are just ones used for recursion

CakePHP Model Query Return Data Formating

I'm looking for a way to make it so cake returns all database data in the same format/structure... Currently it returns two different types of format depending on the relationship.
If a model 'B' is associated with the current model 'A' being queried it will then place model associations for 'B' underneath it as you can see in [User] below. I want it so that all queries use that structure.
example:
$this->find('all', ....
returns:
Array
(
[0] => Array
(
[UserGroup] => Array
(
[id] => 53
[user_id] => 100003332014851
[media_id] =>
[name] => john
[description] => qwasdfad
)
[User] => Array
(
[id] => 100003332014851
[session_id] => ssm2qbrotmm13ho1ipm8ii2492
[username] =>
[password] => -1
[Planner] => Array
(
)
[Purchase] => Array
(
)
[Listing] => Array
(
)
)
)
I want this to look like:
Array
(
[0] => Array
(
[UserGroup] => Array
(
[id] => 53
[user_id] => 100003332014851
[media_id] =>
[name] => john
[description] => qwasdfad
[User] => Array
(
[id] => 100003332014851
[session_id] => ssm2qbrotmm13ho1ipm8ii2492
[username] =>
[password] => -1
[Planner] => Array
(
)
[Purchase] => Array
(
)
[Listing] => Array
(
)
)
)
)
)
In CakePHP, the find() method return data like your first format. But If you want to format like second one then you have to process it by hand (try to avoid this if possible)
$data = $this->find('all');
$assocs = Set::extract('/User', $data); // extracting all `User` array
foreach($assocs as $key => $assoc) {
unset($data[$key]['User']); // removing the associate `User` from `$data`
$data[$key]['UserGroup']['User'] = $assoc['User']; // adding associate under `UserGroup`
}
ended up doing this... it changes the output to what we need. The top level item does not have a header which is fine I just adjusted our scripts for that... maybe this will help somebody else if they need a custom idea
also no guarantee this covers all possible results but so far it works with all the queries we have.
class AppModel extends Model {
function afterFind($results, $primary) {
//if this is a primary, structure like a secondary so entire site is same format
if ($primary) {
$class = get_class($this);
//simple fix for primary
foreach ($results as $key => $result) {
$result = $this->formatData($result, $class);
$results[$key] = $result;
}
}
return $results;
}
function formatData($result, $class) {
$array = array();
if (isset($result[$class])) {
$array = $result[$class];
unset($result[$class]);
}
$array += $result;
return $array;
}
You can also use contain in this case along with find as UserGroup.User for your desired result

Generate multidimensional array based on a single-level XML file

I am (via a web service) receiving a flat XML file that contains a listing of categories, subcategories, subsubcategories, etc, all at the same level. (i.e. Subcategories are not nested under their parent categories.) This, unfortunately, cannot be changed. I have to work with the data provided.
I'm taking this XML and converting it into an object, then into an array using simplexml_load_string and array_map. This is all working as expected. What I'm left with is a master category array that looks something like this.
Array
(
[0] => Array
(
[CategoryID] => HARDWARE
[Description] => Hardware Issue
)
[1] => Array
(
[CategoryID] => MAC_OSX
[Description] => Mac OSX
[ParentCategoryID] => OS
)
[2] => Array
(
[CategoryID] => OFFICE
[Description] => Microsoft Office
[ParentCategoryID] => SOFTWARE
)
[3] => Array
(
[CategoryID] => OS
[Description] => Operating Systems
[ParentCategoryID] => SOFTWARE
)
[4] => Array
(
[CategoryID] => WIN_7
[Description] => Windows 7
[ParentCategoryID] => OS
)
[5] => Array
(
[CategoryID] => SOFTWARE
[Description] => Software Issue
)
)
As you can see, there are subcategories mixed in there, all keyed off of the ParentCategoryID. Parent Categories have that field omitted.
The CategoryID will always be unique, no matter what level it is on. And the array is sorted alphabetically by the Description. The real array is over 250 categories long, above is an abbreviated version for example sake.
I need to take that master array, loop through it and come up with a new array that looks something like this.
Array
(
[0] => Array
(
[CategoryID] => SOFTWARE
[Description] => Software Issue
[SubCategories] => Array
(
[0] => Array
(
[CategoryID] => OS
[Description] => Operating Systems
[SubCategories] => Array
(
[0] => Array
(
[CategoryID] => WIN_7
[Description] => Windows 7
)
[1] => Array
(
[CategoryID] => MAC_OSX
[Description] => Mac OSX
)
)
)
[1] => Array
(
[CategoryID] => OFFICE
[Description] => Microsoft Office
)
)
)
[1] => Array
(
[CategoryID] => HARDWARE
[Description] => Hardware Issue
)
)
Things I have to keep in mind is there may be infinitely many subcategories (so there isn't a set number of sublevels I can hard-code in). I've been trying to mess around with array_search and array_filter, but I'm just not having any luck getting something working. I obviously don't want to loop hundreds of times for this process to happen.
Does anyone with a little more experience with multidimensional arrays under their belt have some ideas, direction, or an example that may help me achieve my desired result?
OK, I figured out an algorithm (i think). The key is to build a linked list, and use placeholders for parent categories later in your list.
Create a category obect, which includes all the relevant data, a link to its parent and an array of links to its children. These will all go into a 1d array
Next loop through your inputs and:
If there is a parent category, check if we have an object for it already. If not, create a placeholder for the parent.
If we don't have an object for it yet, create an object for the category.
Set the parent/child relationships in both objects.
Finally, loop through your array again, and add any object without a parent to a new array.
This new array should be a list of all the parent categories, and usign the relationship you defined you should be able to browse it like a tree. You could also do another pass and build a native 2d array if you need.
I finally got it! It seems so simple now, I'm almost embarrassed to say it took so long to figure out. This was my final code to accomplish my goal.
if($cats['HasError'] == "false") {
$cats = $cats['Support_SubjectsList']['Support_Subjects'];
//Generate a hierarchy array of categories
$count = count($cats);
$i=0;
while($count > 0) {
foreach($cats as $k => $v) {
if($i==0) {
//Parents
if(is_array($v['ParentCategoryID'])) {
$categories[$i][$v['CategoryID']] = array("Description" => $v['Description']);
unset($cats[$k]);
}
} else {
//Children
if(array_key_exists($v['ParentCategoryID'], $categories[($i-1)])) {
$categories[$i][$v['CategoryID']] = array("Description" => $v['Description'], "ParentCategoryID" => $v['ParentCategoryID']);
unset($cats[$k]);
}
}
}
$count = count($cats);
$i++;
}
//Traverse the hierarchy array backwards to make a nested category array
$count = count($categories)-1;
for($i=$count;$i>0;$i--) {
foreach($categories[$i] as $k => $v) {
$categories[($i-1)][$v['ParentCategoryID']]['SubCategories'][$k] = array("Description" => $v['Description']);
if(is_array($v['SubCategories'])) {
$categories[($i-1)][$v['ParentCategoryID']]['SubCategories'][$k]['SubCategories'] = $v['SubCategories'];
}
}
unset($categories[$i]);
}
$categories = $categories[0];
}

Categories