I use PHP and mySQL with Idiorm. That might not be relevant.
My PHP array
It's a relationship between parents and childs.
0 is the root parent.
Example: Root parent 0 have the child 33 which have the child 27 which have
the child 71.
This array structure can be changed if needed for solving the problem.
array (
33 =>
array (
0 => '27',
1 => '41',
),
27 =>
array (
0 => '64',
1 => '71',
),
0 =>
array (
0 => '28',
1 => '29',
2 => '33',
),
)
My hierarchical result
Something like this, but as an array...
0 =>
28
29
33
27 =>
64
71
41
Information
The depth are unkown and it can be unlimited. I tried foreach, but it might not be the way.
My own thoughts
Some recursive function?
Some while loops?
I tried both of the above, just got a mess. It's a brainer.
The suggestion by #deceze worked. However the input array needs to change a litte, like this...
$rows = array(
array(
'id' => 33,
'parent_id' => 0,
),
array(
'id' => 34,
'parent_id' => 0,
),
array(
'id' => 27,
'parent_id' => 33,
),
array(
'id' => 17,
'parent_id' => 27,
),
);
From https://stackoverflow.com/a/8587437/476:
function buildTree(array $elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = buildTree($elements, $element['id']);
if ($children) {
$element['children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
$tree = buildTree($rows);
print_r( $tree );
I added to #Jens Törnell's answers to enable defining the options for the column name of parent_id, the children array key name, and also the column name for id.
/**
* function buildTree
* #param array $elements
* #param array $options['parent_id_column_name', 'children_key_name', 'id_column_name']
* #param int $parentId
* #return array
*/
function buildTree(array $elements, $options = [
'parent_id_column_name' => 'parent_id',
'children_key_name' => 'children',
'id_column_name' => 'id'], $parentId = 0)
{
$branch = array();
foreach ($elements as $element) {
if ($element[$options['parent_id_column_name']] == $parentId) {
$children = buildTree($elements, $options, $element[$options['id_column_name']]);
if ($children) {
$element[$options['children_key_name']] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
Since the functionality is quite universal, I managed to use the above function in most of my projects.
great answer from #Jens Törnell, just wanted to add a little improvement that if your parent_id and id is actually string instead of number then above method will fail and after creating children array, it will create those childrens arrays again as separate individual array. In order to fix that you should do triple equal check and by telling data type of variable i.e (string) in comparison.
For string based Id and Parent_id in array
function buildTree(array $elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ((string)$element['parent_id'] === (string)$parentId) {
$children = buildTree($elements, $element['id']);
if ($children) {
$element['children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
additionally if someone desire, he can add a third parameter to function as well to specify data type of variables dynamically i.e function buildTree(array $elements, $parentId = 0, $datatype='string') but then you will have to take of any other error occur.
hope it will help someone!
public function createTree (&$list, $parentId = null) {
$tree = array();
foreach ($list as $key => $eachNode) {
if ($eachNode['parentId'] == $parentId) {
$eachNode['children'] = $this->createTree ($list,$eachNode['id']);
$tree[] = $eachNode;
unset($list[$key]);
}
}
return $tree;
}
In that function pass the associative array and if the most parent is not null then just pass the most parent id as second argument.
I had a different problem and could not find a solution that worked for me on this page. I needed to create a tree but without knowing the root id.
This means I have to go through my flat array and build branches with the most parently items at the top of the tree.
If anyone else needs to build a tree without a root parent item id, here's how I did it.
<?php
$rows = [
(object) [
'id' => 1001,
'parentid' => 1000,
'name' => 'test1.1'
],
(object) [
'id' => 1000,
'parentid' => 100,
'name' => 'test1'
],
(object) [
'id' => 1002,
'parentid' => 1000,
'name' => 'test1.2'
],
(object) [
'id' => 1004,
'parentid' => 1001,
'name' => 'test1.1.1'
],
(object) [
'id' => 1005,
'parentid' => 1004,
'name' => 'test1.1.1.1'
],
(object) [
'id' => 100,
'parentid' => 10,
'name' => 'test 0'
],
(object) [
'id' => 1006,
'parentid' => 1002,
'name' => 'test1.2.1'
],
(object) [
'id' => 1007,
'parentid' => 1002,
'name' => 'test1.2.2'
],
];
function add_child(stdClass $parent, stdClass $child) {
if ($child->parentid != $parent->id) {
throw new Exception('Attempting to add child to wrong parent');
}
if (empty($parent->children)) {
$parent->children = [];
} else {
// Deal where already in branch.
foreach ($parent->children as $idx => $chd) {
if ($chd->id === $child->id) {
if (empty($chd->children)) {
// Go with $child, since $chd has no children.
$parent->children[$idx] = $child;
return;
} else {
if (empty($child->children)) {
// Already has this child with children.
// Nothing to do.
return;
} else {
// Both childs have children - merge them.
$chd->children += $child->children;
$parent->children[$idx] = $child;
return;
}
}
}
}
}
$parent->children[] = $child;
}
function build_branch(&$branch, &$rows, &$parent = null) {
$hitbottom = false;
while (!$hitbottom) {
$foundsomething = false;
// Pass 1 - find children.
$removals = []; // Indexes of rows to remove after this loop.
foreach ($rows as $idx => $row) {
if ($row->parentid === $branch->id) {
// Found a child.
$foundsomething = true;
// Recurse - find children of this child.
build_branch($row, $rows, $branch);
add_child($branch, $row);
$removals[] = $idx;
}
}
foreach ($removals as $idx) {
unset($rows[$idx]);
}
// Pass 2 - find parents.
if ($parent === null) {
$foundparent = false;
foreach ($rows as $idx => $row) {
if ($row->id === $branch->parentid) {
// Found parent
$foundsomething = true;
$foundparent = true;
add_child($row, $branch);
unset ($rows[$idx]);
// Now the branch needs to become the parent since parent contains branch.
$branch = $row;
// No need to search for other parents of this branch.
break;
}
}
}
$hitbottom = !$foundsomething;
}
}
function build_tree(array $rows) {
$tree = [];
while (!empty($rows)) {
$row = array_shift($rows);
build_branch($row, $rows);
$tree[] = $row;
}
return $tree;
}
$tree = build_tree($rows);
print_r($tree);
Related
I want to transform an array of resources that can have an infinity of children to a simple array like below. I just want to keep the information of the parent, if there is a parent. In my context, a parent is the array just above the child array.
I have this array (bigger in reality with a lof of children), but each children may have an infinity of arrays children:
$array = array (
0 =>
array (
'#id' => 'Authorization',
'#sortOrder' => '1',
'resource' =>
array (
'#id' => 'Authorization2',
'#title' => 'Authorization2',
),
),
);
And I would like to get this, recursively :
$resources = [
0 => [
'parent' => null,
'resource' => 'Authorization'],
1 => [
'Authorization' => 'Authorization',
'resource' => 'Authorization2']
];
I tried this and I get every single resource but I can't get parents for resources that has one:
public function array_values_recursive($array) {
$flat = array();
foreach($array as $key => $value) {
if (is_array($value)) {
$flat = array_merge($flat, $this->array_values_recursive($value));
}
else {
if($key === '#id') {
$flat[]['value'] = $value;
}
}
}
return $flat;
}
That did the job for me, thanks #Sammitch for the idea.
public function array_values_recursive($array, $parent = null) {
$flat = array();
$i = 0;
foreach($array as $key => $value) {
if (is_array($value)) {
//we create a new parent
if(array_key_exists('#id',$array)){
$flat = array_merge($flat, $this->array_values_recursive($value, $array['#id']));
}
//we keep the last parent known
else{
$flat = array_merge($flat, $this->array_values_recursive($value, $parent));
}
}
else {
if($key === '#id') {
if($parent){
$flat[$i]['value'] = $value;
$flat[$i]['parent'] = $parent;
}
else{
$flat[$i]['value'] = $value;
}
$i++;
}
}
}
return $flat;
}
I suffered for days looking for an answer and trying to solve the problme myself but I could not.
I have data in a PHP array with a the key parent_id as an array. I found how to build a tree but only if it has only ONE parent! But in my case it has multiple parents and it must be nested below every parent.
Here is an example:
Parents
array(
'id' => 1,
'name' => 'Parent 1',
'parent_id' => array()
);
array(
'id' => 2,
'name' => 'Parent 2',
'parent_id' => array()
);
Children
array(
'id' => 3,
'name' => 'Child 1',
'parent_id' => array(1, 2)
);
I want the tree to be built like so:
array(
'id' => 1,
'name' => 'Parent 1',
'parent_id' => array(),
'children' => array(
array(
'id' => 3,
'name' => 'Child 1',
'parent_id' => array(1, 2)
)
),
);
array(
'id' => 2,
'name' => 'Parent 2',
'parent_id' => array(),
'children' => array(
array(
'id' => 3,
'name' => 'Child 1',
'parent_id' => array(1, 2)
)
),
);
Can you suggest a working function that may help me. Thanks in advance.
EDITED (DEMO)
#Jeff Lambert is right. What I did was to loop through elements and if any has parents, I add its ID to a newly created key children .. This way I can retrieve it whenever I want.
function build_tree(array $elements)
{
$indexed = array();
foreach($elements as $element)
{
$element = (array) $element;
$indexed[$element['id']] = $element;
}
$elements = $indexed;
unset($indexed, $element);
foreach($elements as $id => $element)
{
if ( ! empty($element['parent_id']))
{
foreach($element['parent_id'] as $parent)
{
if (isset($elements[$parent]))
{
$elements[$parent]['children'][] = $element['id'];
}
}
}
}
return $elements;
}
Then I only need to create and little function to retrieve element details like so:
function get_element($id, $return = NULL)
{
// Check the element inside the array
if (isset($elements[$id])
{
// In case I want to return a single value
if ($return !== NULL and isset($elements[$id][$return])
{
return $elements[$id][$return];
}
return $elements[$id];
}
return FALSE; // Or NULL, as you wish
}
Update
If you want/need to nest the nodes (eg a parent can have 1 or more child nodes, while at the same time be a child node of another parent), then the easiest approach would be to assign references to nodes.
I've hacked together a quick demo that is almost identical to my original approach, apart from that it uses references instead of assigning by value. The code looks like this:
function buildTree(array $data)
{
$data = array_column($data, null, 'id');
//reference to each node in loop
foreach ($data as &$node) {
if (!$node['parent_id']) {
//record has no parents - null or empty array
continue; //skip
}
foreach ($node['parent_id'] as $id) {
if (!isset($data[$id])) { // make sure parent exists
throw new \RuntimeException(
sprintf(
'Child id %d is orphaned, no parent %d found',
$node['id'], $id
)
);
}
if (!isset($data[$id]['children']) {
$data[$id]['children'] = array();
}
$data[$id]['children'][] = &$node; //assign a reference to the child node
}
}
return $data;
}
The double reference is required here, because if you did not use the foreach ($data as &$node), the $node variable would be a copy of the original node. Assigning a reference to the copy wouldn't do you any good. In fact, it'd produce the wrong results.
Likewise, if you did not assign a reference to the &$node from the loop, you'd not get the full list of child nodes throughout the tree.
It's not the easiest thing to explain, but the net result speaks for itself: using the references here allows you to build the tree in full in a single function call.
Here's what I'd do. First, I'd use the id's as array keys, so I can more easily find the parents for each child:
$parents = array_column($parents, null, 'id');
if you're on an older version of PHP, and can't upgrade, this is the equivalent of writing:
$indexed = array();
foreach ($parents as $parent) {
$indexed[$parent['id']] = $parent;
}
$parents = $indexed;
Now iterate over the children, and assign them to their parents:
foreach ($children as $child) {
foreach ($child['parent_id'] as $id) {
if (!isset($parents[$id]['children']) {
$parents[$id]['children'] = array();//ensure the children key exists
}
$parents[$id]['children'][] = $child;//append child to parent
}
}
It really doesn't matter if $parents and $children are 2 separate arrays, or both records are in one big array here.
So a function in case the parent and children are in separate arrays would look like this:
function buildTree(array $parents, array $children)
{
$parents = array_column($parents, null, 'id');
foreach ($children as $child) {
foreach ($child['parent_id'] as $id) {
if (!isset($parents[$id])) { // make sure parent exists
throw new \RuntimeException(
sprintf(
'Child id %d is orphaned, no parent %d found',
$child['id'], $id
)
);
}
if (!isset($parents[$id]['children']) {
$parents[$id]['children'] = array();
}
$parents[$id]['children'][] = $child;
}
}
return $parents;
}
If all of the data is in a single array, then the function will look pretty much the same:
function buildTree(array $data)
{
$data = array_column($data, null, 'id');
foreach ($data as $node) {
if (!$node['parent_id']) {
//record has no parents - null or empty array
continue; //skip
}
foreach ($node['parent_id'] as $id) {
if (!isset($data[$id])) { // make sure parent exists
throw new \RuntimeException(
sprintf(
'Child id %d is orphaned, no parent %d found',
$node['id'], $id
)
);
}
if (!isset($data[$id]['children']) {
$data[$id]['children'] = array();
}
$data[$id]['children'][] = $node;
}
}
return $data;
}
that's how it gonna work correctly.
$arr = array(
array('id'=>100, 'parentid'=>0, 'name'=>'a'),
array('id'=>101, 'parentid'=>100, 'name'=>'a'),
array('id'=>102, 'parentid'=>101, 'name'=>'a'),
array('id'=>103, 'parentid'=>101, 'name'=>'a'),
);
$new = array();
foreach ($arr as $a){
$new[$a['parentid']][] = $a;
}
$tree = createTree($new, array($arr[0]));
print_r($tree);
function createTree(&$list, $parent){
$tree = array();
foreach ($parent as $k=>$l){
if(isset($list[$l['id']])){
$l['children'] = createTree($list, $list[$l['id']]);
}
$tree[] = $l;
}
return $tree;
}
The solution using in_array function:
// $parents and $children are arrays of 'parents' and 'children' items respectively
$tree = [];
foreach ($parents as $p) {
$treeItem = $p + ['children' => []];
foreach ($children as $c) {
if (in_array($p['id'], $c['parent_id']))
$treeItem['children'][] = $c;
}
$tree[] = $treeItem;
}
print_r($tree);
DEMO link
A tree where each node can have multiple parents is not a tree, but a graph. One way to represent a graph is via an adjacency list.
As it is, you're storing the 'children' of each node within that node index, and you shouldn't because each node will be duplicated the same number of times as other nodes it is connected to. Each node should be represented at the top level of your structure, and contain references to the other nodes they happen to be connected to, in your case your 'parent_id' index. I would separate out the actual definitions of your nodes and declare what other nodes each one is connected to in separate structures.
Something along these lines for defining your nodes:
array(
0 => array(
'id' => 1,
'name' => 'Parent 1',
),
1 => array(
'id' => 2,
'name' => 'Parent 2',
),
2 => array(
'id' => 3,
'name' => 'Child 1',
),
)
And then a separate array looking something like this for defining the connections between nodes:
array(
// These indices match the node indices above, and the values are the list of
// node indices each node has a connection to.
0 => array(2),
1 => array(2),
2 => array(0, 1),
)
Then it should be easy to find and implement any sort of traversal algorithm you may need.
$data = [
['id' => 1, 'parent' => []],
['id' => 2, 'parent' => [1]],
['id' => 3, 'parent' => [2,4]],
['id' => 4, 'parent' => []]
];
$result = [];
foreach ($data as $item) {
if(!count($item['parent'])) {
makeTree($result, $item, $data);
}
}
print_r($result);
function makeTree(&$result, $item, $data) {
$result['children'][$item['id']]['data'] = $item;
if(haveChildren($item['id'], $data)) {
foreach(children($item['id'], $data) as $child) {
makeTree($result['children'][$item['id']], $child, $data);
}
}
}
function children($id, $data){
$result = [];
foreach($data as $item) {
if(in_array($id, $item['parent'])) {
$result[] = $item;
}
}
return $result;
}
function haveChildren($id, $data) {
foreach($data as $item) {
if(in_array($id, $item['parent'])) {
return true;
}
}
}
I'm trying to produce a multi-level HTML list from a source array that is formatted like this:
/**
* id = unique id
* parent_id = "id" that this item is directly nested under
* text = the output string
*/
$list = array(
array(
'id' => 1,
'parent_id' => 0,
'text' => 'Level 1',
), array(
'id' => 2,
'parent_id' => 0,
'text' => 'Level 2',
), array(
'id' => 3,
'parent_id' => 2,
'text' => 'Level 2.1',
), array(
'id' => 4,
'parent_id' => 2,
'text' => 'Level 2.2',
), array(
'id' => 5,
'parent_id' => 4,
'text' => 'Level 2.2.1',
), array(
'id' => 6,
'parent_id' => 0,
'text' => 'Level 3',
)
);
The goal is a nested <ul> with an infinite depth. The expected output of the array above is this:
Level 1Level 2Level 2.1Level 2.2Level 2.2.1Level 3
If only the array items had a key called child or something that contained the actual sub-array, it would be easy to recurse though these and get the desired output with a function like this:
function makeList($list)
{
echo '<ul>';
foreach ($list as $item)
{
echo '<li>'.$item['text'];
if (isset($item['child']))
{
makeList($item['child']);
}
echo '</li>';
}
echo '</ul>';
}
Unfortunately that's not the case for me - the format of the source arrays can't be changed. So, long ago I wrote this very nasty function to make it happen, and it only works up to three levels (code is pasted verbatim with original comments). I know it's a long boring read, please bear with me:
function makeArray($links)
{
// Output
$nav = array();
foreach ($links as $k => $v)
{
// If no parent_id is present, we can assume it is a top-level link
if (empty($v['parent_id']))
{
$id = isset($v['id']) ? $v['id'] : $k;
$nav[$id] = $v;
// Remove from original array
unset($links[$k]);
}
}
// Loop through the remaining links again,
// we can assume they all have a parent_id
foreach ($links as $k => $v)
{
// Link's parent_id is in the top level array, so this is a level-2 link
// We already looped through every item so we know they are all accounted for
if (isset($nav[$v['parent_id']]))
{
$id = isset($v['id']) ? $v['id'] : $k;
// Add it to the top level links as a child
$nav[$v['parent_id']]['child'][$id] = $v;
// Set a marker so we know which ones to loop through to add the third level
$nav2[$id] = $v;
// Remove it from the array
unset($links[$k]);
}
}
// Last iteration for the third level
// All other links have been removed from the original array at this point
foreach ($links as $k => $v)
{
$id = isset($v['id']) ? $v['id'] : $k;
// Link's parent_id is in the second level array, so this is a level-3 link
// Orphans will be ignored
if (isset($nav2[$v['parent_id']]))
{
// This part is crazy, just go with it
$nav3 = $nav2[$v['parent_id']]['parent_id'];
$nav[$nav3]['child'][$v['parent_id']]['child'][] = $v;
}
}
return $nav;
}
This makes an array like:
array(
'text' => 'Level 1'
'child' => array(
array(
'text' => 'Level 1.2'
'child' => array(
array(
'text' => 'Level 1.2.1'
'child' => array(
// etc.
),
array(
'text' => 'Level 1.2.2'
'child' => array(
// etc.
),
)
)
)
)
);
Usage:
$nav = makeArray($links);
makeList($nav);
I've spent many spare hours trying to work this out, and the original code which I have given here is still the best solution I've been able to produce.
How can I make this happen without that awful function (which is limited to a depth of 3), and have an infinite number of levels? Is there a more elegant solution to this?
Print:
function printListRecursive(&$list,$parent=0){
$foundSome = false;
for( $i=0,$c=count($list);$i<$c;$i++ ){
if( $list[$i]['parent_id']==$parent ){
if( $foundSome==false ){
echo '<ul>';
$foundSome = true;
}
echo '<li>'.$list[$i]['text'].'</li>';
printListRecursive($list,$list[$i]['id']);
}
}
if( $foundSome ){
echo '</ul>';
}
}
printListRecursive($list);
Create multidimensional array:
function makeListRecursive(&$list,$parent=0){
$result = array();
for( $i=0,$c=count($list);$i<$c;$i++ ){
if( $list[$i]['parent_id']==$parent ){
$list[$i]['childs'] = makeListRecursive($list,$list[$i]['id']);
$result[] = $list[$i];
}
}
return $result;
}
$result = array();
$result = makeListRecursive($list);
echo '<pre>';
var_dump($result);
echo '</pre>';
Tested and working :)
$list = array(...);
$nested = array();
foreach ($list as $item)
{
if ($item['parent_id'] == 0)
{
// Woot, easy - top level
$nested[$item['id']] = $item;
}
else
{
// Not top level, find it's place
process($item, $nested);
}
}
// Recursive function
function process($item, &$arr)
{
if (is_array($arr))
{
foreach ($arr as $key => $parent_item)
{
// Match?
if (isset($parent_item['id']) && $parent_item['id'] == $item['parent_id'])
{
$arr[$key]['children'][$item['id']] = $item;
}
else
{
// Keep looking, recursively
process($item, $arr[$key]);
}
}
}
}
Some methods I recently wrote, maybe some will help, sorry I'm short on time and cannot rewite them to match your needs.
This code is actually a part of Kohana Framework Model, method ->as_array() is used to flat an Database_Result object.
function array_tree($all_nodes){
$tree = array();
foreach($all_nodes as $node){
$tree[$node->id]['fields'] = $node->as_array();
$tree[$node->id]['children'] = array();
if($node->parent_id){
$tree[$node->parent_id]['children'][$node->id] =& $tree[$node->id];
}
}
$return_tree = array();
foreach($tree as $node){
if($node['fields']['depth'] == 0){
$return_tree[$node['fields']['id']] = $node;
}
}
return $return_tree;
}
array_tree() is used to make a tree out of a flat array. The key feature is the =& part ;)
function html_tree($tree_array = null){
if( ! $tree_array){
$tree_array = $this -> array_tree();
}
$html_tree = '<ul>'."\n";
foreach($tree_array as $node){
$html_tree .= $this->html_tree_crawl($node);
}
$html_tree .= '</ul>'."\n";
return $html_tree;
}
function html_tree_crawl($node){
$children = null;
if(count($node['children']) > 0){
$children = '<ul>'."\n";
foreach($node['children'] as $chnode){
$children .= $this->html_tree_crawl($chnode);
}
$children .= '</ul>'."\n";
}
return $this->html_tree_node($node, $children);
}
html_tree_node() is a simple method to display current node and children in HTML.
Example below:
<li id="node-<?= $node['id'] ?>">
<?= $node['title'] ?>
<?= (isset($children) && $children != null) ? $children : '' ?>
</li>
So I have an array of items in php, some may be linked to others via a parent_id key. I'm looking to sort this array so that any items whose parent is in this array ends up positioned right below the parent.
example: (actual array has many more keys)
some_array[0]['id'] = 15001;
some_array[0]['parent_id'] = 14899;
some_array[1]['id'] = 14723;
some_array[1]['parent_id'] = 0; //parent_id of 0 means item has no parent of its own
some_array[2]['id'] = 14899;
some_array[2]['parent_id'] = 0;
some_array[3]['id'] = 15000;
some_array[3][parent_id'] = 14723;
I'd like to sort these so they end up in this order:
some_array[0]['id'] = 14723;
some_array[1]['id'] = 15000;
some_array[2]['id'] = 14899;
some_array[3]['id'] = 15001;
ie. items are just below their parents.
Thanks in advance!
My shorter version of mattwang's answer:
/**
* sort parents before children
*
* #param array $objects input objects with attributes 'id' and 'parent'
* #param array $result (optional, reference) internal
* #param integer $parent (optional) internal
* #param integer $depth (optional) internal
* #return array output
*/
function parent_sort(array $objects, array &$result=array(), $parent=0, $depth=0) {
foreach ($objects as $key => $object) {
if ($object->parent == $parent) {
$object->depth = $depth;
array_push($result, $object);
unset($objects[$key]);
parent_sort($objects, $result, $object->id, $depth + 1);
}
}
return $result;
}
Only actual difference is that it sorts an array of objects instead of an array of arrays.
I doubt that you guys are still looking for a real answer to this, but it might help out others with the same problem. Below is a recursive function to resort an array placing children beneath parents.
$initial = array(
array(
'name' => 'People',
'ID' => 2,
'parent' => 0
),
array(
'name' => 'Paul',
'ID' => 4,
'parent' => 2
),
array(
'name' => 'Liz',
'ID' => 5,
'parent' => 2
),
array(
'name' => 'Comus',
'ID' => 6,
'parent' => 3
),
array(
'name' => 'Mai',
'ID' => 7,
'parent' => 2
),
array(
'name' => 'Titus',
'ID' => 8,
'parent' => 3
),
array(
'name' => 'Adult',
'ID' => 9,
'parent' => 6
),
array(
'name' => 'Puppy',
'ID' => 10,
'parent' => 8
),
array(
'name' => 'Programmers',
'ID' => 11,
'parent' => 4
) ,
array(
'name' => 'Animals',
'ID' => 3,
'parent' => 0
)
);
/*---------------------------------
function parentChildSort_r
$idField = The item's ID identifier (required)
$parentField = The item's parent identifier (required)
$els = The array (required)
$parentID = The parent ID for which to sort (internal)
$result = The result set (internal)
$depth = The depth (internal)
----------------------------------*/
function parentChildSort_r($idField, $parentField, $els, $parentID = 0, &$result = array(), &$depth = 0){
foreach ($els as $key => $value):
if ($value[$parentField] == $parentID){
$value['depth'] = $depth;
array_push($result, $value);
unset($els[$key]);
$oldParent = $parentID;
$parentID = $value[$idField];
$depth++;
parentChildSort_r($idField,$parentField, $els, $parentID, $result, $depth);
$parentID = $oldParent;
$depth--;
}
endforeach;
return $result;
}
$result = parentChildSort_r('ID','parent',$initial);
print '<pre>';
print_r($result);
print '</pre>';
It's a wind down method that removes elements from the original array and places them into result set in the proper order. I made it somewhat generic for you, so it just needs you to tell it what your 'ID' field and 'parent' fields are called. Top level items are required to have a parent_id (however you name it) of 0.
You can use usort to sort by a user defined function:
function cmp($a, $b)
{
if ( $a['id'] == $b['id'] ) {
return 0;
} else if ( $a['parent_id'] ) {
if ( $a['parent_id'] == $b['parent_id'] ) {
return ( $a['id'] < $b['id'] ? -1 : 1 );
} else {
return ( $a['parent_id'] >= $b['id'] ? 1 : -1 );
}
} else if ( $b['parent_id'] ) {
return ( $b['parent_id'] >= $a['id'] ? -1 : 1);
} else {
return ( $a['id'] < $b['id'] ? -1 : 1 );
}
}
usort($some_array, "cmp");
Note: this will only work with a tree that is one level deep (meaning no children of children). For more complex trees you probably want to sort the data into a graph and then flatten it.
Edit: fixed to edit a case where $b has a parent but $a does not.
Just use usort() function and compare two different elements of the 'big array' in a way you need. This becomes then a question about 'how do I really decide which element is before which element?'.
The simple usort won't work if you want to support more than one layer of children. There's simply no way to know how two arbitrary elements compare without other information.
I didn't think about it much, so perhaps it doesn't work. But here's a sorting class:
class TopSort
{
private $sorted, $unsorted;
private $history;
public function sort(array $unsorted)
{
$this->sorted = array();
$this->unsorted = $unsorted;
$this->history = array();
usort($this->unsorted, function($a, $b)
{
return $b['id'] - $a['id'];
});
foreach ($this->unsorted as $i => $a)
if ($a['parent_id'] == 0) $this->visit($i);
return array_reverse($this->sorted);
}
private function visit($i)
{
if (!array_key_exists($i, $this->history))
{
$this->history[$i] = true;
foreach ($this->unsorted as $j => $a)
if ($a['parent_id'] == $this->unsorted[$i]['id']) $this->visit($j);
$this->sorted[] = $this->unsorted[$i];
}
}
}
$sorter = new TopSort();
$some_array = $sorter->sort($some_array);
The idea here is to first sort in reverse by id. Then build up a new array by inserting the deepest elements (those with no children) first. Since we initially sorted the array by reverse id, it should mean the entire thing is upside down. After reversing the array, it should be exactly like you want. (Of course one could unshift items onto the array to avoid the reverse operation, but that might be slower...)
And this is very unoptimized as it iterates over the entire array many, many times. With a little rework, it wouldn't need to do that.
Here's an alternative class that is more optimized:
class TopSort
{
private $sorted;
public function sort(array $nodes)
{
$this->sorted = array();
# sort by id
usort($nodes, function($a, $b) {
return $a['id'] - $b['id'];
});
# build tree
$p = array(0 => array());
foreach($nodes as $n)
{
$pid = $n['parent_id'];
$id = $n['id'];
if (!isset($p[$pid]))
$p[$pid] = array('child' => array());
if (isset($p[$id]))
$child = &$p[$id]['child'];
else
$child = array();
$p[$id] = $n;
$p[$id]['child'] = &$child;
unset($child);
$p[$pid]['child'][] = &$p[$id];
}
$nodes = $p['0']['child'];
unset($p);
# flatten array
foreach ($nodes as $node)
$this->flatten($node);
return $this->sorted;
}
private function flatten(array $node)
{
$children = $node['child'];
unset($node['child']);
$this->sorted[] = $node;
foreach ($children as $node)
$this->flatten($node);
}
}
$sorter = new TopSort();
$sorted = $sorter->sort($some_array);
It's a three step approach:
Sort by id (usort)
Build nested array structure.
Flatten array in pre-order.
By virtue of presorting by id, each group of children should be sorted correctly.
I'm trying to create a list of categories with any number of sub categories, where sub categories can also has their own sub categories.
I have selected all categories from the Mysql db, the cats are in a standard associate array list, each category has an id, name, parentid where the parentid is 0 if it's top level.
I basically want to be able to take the single level array of cats and turn it into a multidimensional array structure where each category can have an element which will contain an array of subcats.
Now, I can easily achieve this by looping a query for each category but this is far from ideal, I'm trying to do it without any extra hits on the db.
I understand I need a recursive function for this. Can anyone point me in the right direction for this tree style structure?
Cheers
This does the job:
$items = array(
(object) array('id' => 42, 'parent_id' => 1),
(object) array('id' => 43, 'parent_id' => 42),
(object) array('id' => 1, 'parent_id' => 0),
);
$childs = array();
foreach($items as $item)
$childs[$item->parent_id][] = $item;
foreach($items as $item) if (isset($childs[$item->id]))
$item->childs = $childs[$item->id];
$tree = $childs[0];
print_r($tree);
This works by first indexing categories by parent_id. Then for each category, we just have to set category->childs to childs[category->id], and the tree is built !
So, now $tree is the categories tree. It contains an array of items with parent_id=0, which themselves contain an array of their childs, which themselves ...
Output of print_r($tree):
stdClass Object
(
[id] => 1
[parent_id] => 0
[childs] => Array
(
[0] => stdClass Object
(
[id] => 42
[parent_id] => 1
[childs] => Array
(
[0] => stdClass Object
(
[id] => 43
[parent_id] => 42
)
)
)
)
)
So here is the final function:
function buildTree($items) {
$childs = array();
foreach($items as $item)
$childs[$item->parent_id][] = $item;
foreach($items as $item) if (isset($childs[$item->id]))
$item->childs = $childs[$item->id];
return $childs[0];
}
$tree = buildTree($items);
Here is the same version, with arrays, which is a little tricky as we need to play with references (but works equally well):
$items = array(
array('id' => 42, 'parent_id' => 1),
array('id' => 43, 'parent_id' => 42),
array('id' => 1, 'parent_id' => 0),
);
$childs = array();
foreach($items as &$item) $childs[$item['parent_id']][] = &$item;
unset($item);
foreach($items as &$item) if (isset($childs[$item['id']]))
$item['childs'] = $childs[$item['id']];
unset($item);
$tree = $childs[0];
So the array version of the final function:
function buildTree($items) {
$childs = array();
foreach($items as &$item) $childs[(int)$item['parent_id']][] = &$item;
foreach($items as &$item) if (isset($childs[$item['id']]))
$item['childs'] = $childs[$item['id']];
return $childs[0]; // Root only.
}
$tree = buildTree($items);
You can fetch all categories at once.
Suppose you have a flat result from the database, like this:
$categories = array(
array('id' => 1, 'parent' => 0, 'name' => 'Category A'),
array('id' => 2, 'parent' => 0, 'name' => 'Category B'),
array('id' => 3, 'parent' => 0, 'name' => 'Category C'),
array('id' => 4, 'parent' => 0, 'name' => 'Category D'),
array('id' => 5, 'parent' => 0, 'name' => 'Category E'),
array('id' => 6, 'parent' => 2, 'name' => 'Subcategory F'),
array('id' => 7, 'parent' => 2, 'name' => 'Subcategory G'),
array('id' => 8, 'parent' => 3, 'name' => 'Subcategory H'),
array('id' => 9, 'parent' => 4, 'name' => 'Subcategory I'),
array('id' => 10, 'parent' => 9, 'name' => 'Subcategory J'),
);
You can create a simple function that turns that flat list into a structure, preferably inside a function. I use pass-by-reference so that there are only one array per category and not multiple copies of the array for one category.
function categoriesToTree(&$categories) {
A map is used to lookup categories quickly. Here, I also created a dummy array for the "root" level.
$map = array(
0 => array('subcategories' => array())
);
I added another field, subcategories, to each category array, and add it to the map.
foreach ($categories as &$category) {
$category['subcategories'] = array();
$map[$category['id']] = &$category;
}
Looping through each categories again, adding itself to its parent's subcategory list. The reference is important here, otherwise the categories already added will not be updated when there are more subcategories.
foreach ($categories as &$category) {
$map[$category['parent']]['subcategories'][] = &$category;
}
Finally, return the subcategories of that dummy category which refer to all top level categories._
return $map[0]['subcategories'];
}
Usage:
$tree = categoriesToTree($categories);
And here is the code in action on Codepad.
See the method :
function buildTree(array &$elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = buildTree($elements, $element['id']);
if ($children) {
$element['children'] = $children;
}
$branch[$element['id']] = $element;
}
}
return $branch;
}
I had the same problem and solved it this way: fetch cat rows from DB and for each root categories, build tree, starting with level (depth) 0. May not be the most efficient solution, but works for me.
$globalTree = array();
$fp = fopen("/tmp/taxonomy.csv", "w");
// I get categories from command line, but if you want all, you can fetch from table
$categories = $db->fetchCol("SELECT id FROM categories WHERE parentid = '0'");
foreach ($categories as $category) {
buildTree($category, 0);
printTree($category);
$globalTree = array();
}
fclose($file);
function buildTree($categoryId, $level)
{
global $db, $globalTree;
$rootNode = $db->fetchRow("SELECT id, name FROM categories WHERE id=?", $categoryId);
$childNodes = $db->fetchAll("SELECT * FROM categories WHERE parentid = ? AND id <> ? ORDER BY id", array($rootNode['id'], $rootNode['id']));
if(count($childNodes) < 1) {
return 0;
} else {
$childLvl = $level + 1;
foreach ($childNodes as $childNode) {
$id = $childNode['id'];
$childLevel = isset($globalTree[$id])? max($globalTree[$id]['depth'], $level): $level;
$globalTree[$id] = array_merge($childNode, array('depth' => $childLevel));
buildTree($id, $childLvl);
}
}
}
function printTree($categoryId) {
global $globalTree, $fp, $db;
$rootNode = $db->fetchRow("SELECT id, name FROM categories WHERE id=?", $categoryId);
fwrite($fp, $rootNode['id'] . " : " . $rootNode['name'] . "\n");
foreach ($globalTree as $node) {
for ($i=0; $i <= $node['depth']; $i++) {
fwrite($fp, ",");
}
fwrite($fp, $node['id'] " : " . $node['name'] . "\n");
}
}
ps. I am aware that OP is looking for a solution without DB queries, but this one involves recursion and will help anybody who stumbled across this question searching for recursive solution for this type of question and does not mind DB queries.
If the parent key is not passed from the class object then my code will create a root category and if the parent value is passed then child will create under the parent root.
class CategoryTree {
public $categories = array();
public function addCategory(string $category, string $parent=null) : void
{
if( $parent ) {
if ( array_key_exists($parent , $this->categories ) ) {
$this->categories[$parent][] = $category;
}
else {
$this->categories[$parent] = array();
$this->categories[$parent][] = $category;
}
}
else {
if ( ! array_key_exists($category , $this->categories ) ) {
$this->categories[$category] = array();
}
}
}
public function getChildren(string $parent = null) : array
{
$data = [];
if ( array_key_exists($parent , $this->categories ) ) {
$data = $this->categories[$parent];
}
return $data;
}
}
$c = new CategoryTree;
$c->addCategory('A', null);
$c->addCategory('B', 'A');
$c->addCategory('C', 'A');
$c->addCategory('C', 'E');
$c->addCategory('D', 'E');
$c->addCategory('D', null);
$c->addCategory('N', 'D');
$c->addCategory('A', null);
$c->addCategory('G', 'A');
echo implode(',', $c->getChildren('A'));