Create html select list from php array? - php

Hello I find php function that outputs html select list from array.
function buildTree(Array $data, $parent = 0) {
$tree = array();
foreach ($data as $d) {
if ($d['parent'] == $parent) {
$children = buildTree($data, $d['id']);
// set a trivial key
if (!empty($children)) {
$d['_children'] = $children;
}
$tree[] = $d;
}
}
return $tree;
}
$rows = array(
array ('id' => 1, 'name' => 'Test 1', 'parent' => 0),
array ('id' => 2, 'name' => 'Test 1.1', 'parent' => 1),
array ('id' => 3, 'name' => 'Test 1.2', 'parent' => 1),
array ('id' => 4, 'name' => 'Test 1.2.1', 'parent' => 3),
array ('id' => 5, 'name' => 'Test 1.2.2', 'parent' => 3),
array ('id' => 6, 'name' => 'Test 1.2.2.1', 'parent' => 5),
array ('id' => 7, 'name' => 'Test 2', 'parent' => 0),
array ('id' => 8, 'name' => 'Test 2.1', 'parent' => 7),
);
$tree = buildTree($rows);
// print_r($tree);
function printTree($tree, $r = 0, $p = null) {
foreach ($tree as $i => $t) {
$dash = ($t['parent'] == 0) ? '' : str_repeat('-', $r) .' ';
printf("\t<option value='%d'>%s%s</option>\n", $t['id'], $dash, $t['name']);
if ($t['parent'] == $p) {
// reset $r
$r = 0;
}
if(isset($t['_children'])){
printTree($t['_children'], ++$r, $t['parent']);
}
}
}
print("<select>\n");
printTree($tree);
print("</select>");
but I need to rewrite to return result like this:
$select = "<select>";
$select .= printTree($list);
$select .= "</select>";
echo $select;
// or better
return $select;
The problem is with recursion, solution is to fill each option in array, but I don't know how to do that in recursive functions, and also
printf("\t<option value='%d'>%s%s</option>\n", $t['id'], $dash, $t['name']);
prints directly when foreach loop iterate.
Thanks.

So i figure out where was my mistake, that was simply because i fill an array with html option tag eg.
<option value="0">Start</option>
but with php function print_r() i see nothing in array value exepts when i inspect DOM element.
so here is my final solution:
this function fill values in multi dimensional array, to further needs
# edited printTree() function, renamed to toSEL()
# $array - data array like above,
# $r - to correct iterate, $p - parent id,
# $currentID - what id is selected
function toSEL($array, $r = 0, $p = null, $currentID=null){
foreach($array as $value){
$dash = ($value[parent] == 0) ? '' : str_repeat('-', $r) .' ';
if($value[id]==$currentID){
$html[] = '<option value="'.$value[id].'" selected="selected">'.$dash.$value[name].'</option>';
}else{
$html[] = '<option value="'.$value[id].'">'.$dash.$value[name].'</option>';
}
if($value['parent'] == $p){
// reset $r
$r = 0;
}
if(!empty($value[children])){
$html[] = toSEL($value[children], ++$r, $value[parent], $currentID);
}
}
return $html;
}
to convert from multi dimensional array to one dimension
$aNonFlat = toSEL($list, 0, null, $currentID);
$result = array();
array_walk_recursive($aNonFlat,function($v, $k) use (&$result){ $result[] = $v; });
then if needs to output HTML use some simple loop.
$html = '<select>';
foreach($result as $res){
$html .= $res;
}
$html .='</select>';
echo $html;

Related

MYSQL database to JSON format using PHP

I tried to convert get data from mysql in json format. For that I am using PHP.
My PHP code is
<?php
define('_HOST_NAME', 'localhost');
define('_DATABASE_USER_NAME', 'root');
define('_DATABASE_PASSWORD', 'admin321');
define('_DATABASE_NAME', 'tree');
$dbConnection = new mysqli(_HOST_NAME,
_DATABASE_USER_NAME, _DATABASE_PASSWORD, _DATABASE_NAME);
if ($dbConnection->connect_error) {
trigger_error('Connection
Failed: ' . $dbConnection->connect_error, E_USER_ERROR);
}
$_GLOBAL['dbConnection'] = $dbConnection;
$categoryList = categoryParentChildTree();
foreach($categoryList as $key => $value){
echo $value['name'].'<br>';
}
function categoryParentChildTree($parent = 0,
$spacing = '', $category_tree_array = '') {
global $dbConnection;
$parent = $dbConnection->real_escape_string($parent);
if (!is_array($category_tree_array))
$category_tree_array = array();
$sqlCategory = "SELECT id,name,parent FROM php WHERE parent = $parent ORDER BY id ASC";
$resCategory=$dbConnection->query($sqlCategory);
if ($resCategory->num_rows != null && $resCategory->num_rows>0) {
while($rowCategories = $resCategory->fetch_assoc()) {
$category_tree_array[] = array("id" => $rowCategories['id'], "name" => $spacing . $rowCategories['name']);
$category_tree_array = categoryParentChildTree(
$rowCategories['id'],
' '.$spacing . '- ',
$category_tree_array
);
}
}
return $category_tree_array;
}
?>
mysql table
ID PARENT NAME
1 0 category
2 1 fruit
3 2 apple
4 2 orange
5 1 animals
6 5 tiger
7 5 lion
8 1 car
My output is:
category
- fruit
- - apple
- - orange
- animal
- - tiger
- - lion
- cars
I want to get nested json output. Already asked here. No proper response.
I tried with json_encode, not getting nested json.
UPDATED PHP
<?php
$con=mysqli_connect("localhost","root","admin321","tree");
if (mysqli_connect_errno()) //con error
{
echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
$r = mysqli_query($con,"SELECT * FROM php ");
$data = array();
while($row = mysqli_fetch_assoc($r)) {
$data[] = $row;
}
function buildtree($src_arr, $parent_id = 0, $tree = array())
{
foreach($src_arr as $idx => $row)
{
if($row['parent'] == $parent_id)
{
foreach($row as $k => $v)
$tree[$row['id']][$k] = $v;
unset($src_arr[$idx]);
$tree[$row['id']]['children'] = buildtree($src_arr, $row['id']);
}
}
ksort($tree);
return $tree;
}
function insertIntoNestedArray(&$array, $searchItem){
// Insert root element
if($searchItem['parent'] == 0){
array_push($array, $searchItem);
return;
}
if(empty($array)){ return; }
array_walk($array, function(&$item, $key, $searchItem){
if($item['id'] == $searchItem['parent']){
array_push($item['children'], $searchItem);
return;
}
insertIntoNestedArray($item['children'], $searchItem);
}, $searchItem);
}
$nestedArray = array();
foreach($data as $itemData){
// First: Mount the nested array item
$nestedArrayItem['id'] = $itemData['id'];
$nestedArrayItem['name'] = $itemData['name'];
$nestedArrayItem['parent'] = $itemData['parent'];
$nestedArrayItem['children'] = array();
// Second: insert the item into the nested array
insertIntoNestedArray($nestedArray, $nestedArrayItem);
}
$json = json_encode($nestedArray);
echo $json;
?>
Simply this: json_encode($output , JSON_FORCE_OBJECT);
Your Nested Output is just an human-representation of the stored data you have in your database. Its a human thing. Machines can't understand that, that's why in mysql you need a column to tell the category parent.
So, your problem is that you're trying to convert to JSON your already manipulated data. You need to convert to JSON your raw data, and then manipulate it in the code that receives the JSON.
use json_encode to encode the raw data:
$raw_data = $resCategory->fetch_all();
return json_encode($raw_data);
Also, just a note: this $_GLOBAL variable you're using... you're not trying to refer to the internal php $GLOBALS superglobal, you are?
EDIT:
Ok, now that you explained that you need the nested json format, you will need to use some recursion to build an nested array of arrays and then use the json_encode on it.
First: Get the raw data:
$resCategory=$dbConnection->query($sqlCategory);
$raw_data = $resCategory->fetch_all();
Now, suppose this $raw_data variable returns an array like this:
array (
0 => array (
'ID' => 1,
'PARENT' => 0,
'NAME' => 'category',
),
1 => array (
'ID' => 2,
'PARENT' => 1,
'NAME' => 'fruit',
),
2 => array (
'ID' => 3,
'PARENT' => 2,
'NAME' => 'apple',
),
3 => array (
'ID' => 4,
'PARENT' => 2,
'NAME' => 'orange',
),
4 => array (
'ID' => 5,
'PARENT' => 1,
'NAME' => 'animals',
),
5 => array (
'ID' => 6,
'PARENT' => 5,
'NAME' => 'tiger',
),
6 => array (
'ID' => 7,
'PARENT' => 5,
'NAME' => 'lion',
),
7 => array (
'ID' => 8,
'PARENT' => 1,
'NAME' => 'car',
)
)
Second: Build up an recursive function to insert items of this array into another array, the $nestedArray (that we will create in the third step).
function insertIntoNestedArray(&$array, $searchItem){
// Insert root element
if($searchItem['parent'] == 0){
array_push($array, $searchItem);
return;
}
// Stop the recursion when the array to check is empty
if(empty($array)){ return; }
// Walk the array searching for the parent of the search item
array_walk($array, function(&$item, $key, $searchItem){
// If the parent is found, then append the search item to it
if($item['id'] == $searchItem['parent']){
array_push($item['children'], $searchItem);
return;
}
// If the parent wasn't found, walk thought the children of the array
insertIntoNestedArray($item['children'], $searchItem);
}, $searchItem);
}
Third: Create the $nestedArray and populate it by loop through the $raw_data array and calling the recursive function:
$nestedArray = array();
foreach($data as $itemData){
// First: Mount the nested array item
$nestedArrayItem['id'] = $itemData['ID'];
$nestedArrayItem['name'] = $itemData['NAME'];
$nestedArrayItem['parent'] = $itemData['PARENT'];
$nestedArrayItem['children'] = array();
// Second: insert the item into the nested array
insertIntoNestedArray($nestedArray, $nestedArrayItem);
}
Fourth: Now its just to json_encode the $nestedArray:
$json = json_encode($nestedArray);
You can do an echo $json and the result will be:
[{"id":1,"name":"category","parent":0,"children":[{"id":2,"name":"fruit","parent":1,"children":[{"id":3,"name":"apple","parent":2,"children":[]},{"id":4,"name":"orange","parent":2,"children":[]}]},{"id":5,"name":"animals","parent":1,"children":[{"id":6,"name":"tiger","parent":5,"children":[]},{"id":7,"name":"lion","parent":5,"children":[]}]},{"id":8,"name":"car","parent":1,"children":[]}]}]

PHP build tree BUT multiple parents in an array

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

PHP hierarchical array - Parents and childs

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

How can I build a nested HTML list with an infinite depth from a flat array?

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>

PHP tree structure for categories and sub categories without looping a query

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

Categories