I have a menu navigation, like this:
- Page 1
- Page 2
- Subpage A
- Subpage B
- Subpage C
- Page 3
- Subpage D
- Subpage E
- Subsubpage I
- Subsubpage II
- Page 4
- Subpage F
But the CMS I use spits out a flat tree, only displaying the parent (not which level they're on). Like so:
$orig_tree = [
[
'db_id' => 1,
'name' => 'Page 1',
'parent' => 0
],
[
'db_id' => 2,
'name' => 'Page 2',
'parent' => 0
],
'db_id' => 3,
'name' => 'Subpage A',
'parent' => 2
],
[
'db_id' => 4,
'name' => 'Subpage B',
'parent' => 2
],
[
'db_id' => 5,
'name' => 'Subpage C',
'parent' => 2
],
[
'db_id' => 6,
'name' => 'Page 3',
'parent' => 0
],
[
'db_id' => 7,
'name' => 'Subpage D',
'parent' => 6
],
[
'db_id' => 8,
'name' => 'Subpage E',
'parent' => 6
],
[
'db_id' => 9,
'name' => 'Subsubpage I',
'parent' => 8
],
[
'db_id' => 10,
'name' => 'Subsubpage II',
'parent' => 8
],
[
'db_id' => 11,
'name' => 'Page 4',
'parent' => 0
],
[
'db_id' => 12,
'name' => 'Subpage F',
'parent' => 11
]
]
These information are stored in the database, so it has to be assembled in runtime (and it's really large), which means that I would like to make it as performance-friendly as possible.
So how do I put together this tree?
Assumptions
I'm pretty sure that the flat original tree always will be in order. So that the first entry always will be a root node (and not one of the children of another node). And that child-nodes will come just after the parent.
At the moment I have at most 3 levels. But it would be cool with a solution that worked with any number of levels.
Attempts
I'm making an object and making methods for all these operations. This is just to illustrate the solution in the fastest manner, to make the problem more digestible.
Attempt 1 - Recursion
Recursion would be the prettiest. I had a few go's at it, but I couldn't get it to work. Here is how far I made it:
__constructor( $orig_tree ){
$final_tree = [];
foreach( $orig_tree as $orig_node ){
$final_tree = $this->addNode( $orig_node, $final_tree );
}
}
private function addNode( $node, $final_tree ){
if( $node['parent'] == 'None' ){
$final_tree[] = $node;
} else {
// This is where I get stuck...
$this->findParentNode( $node, $final_tree ); // This is wrong
}
}
I'm not sure, if I should work it from the root to the leaves or the other way around. Or if the final tree should be passed to the recursive method or not.
Attempt 2 - Brute Force ugly
First assemble all the branches, and then go through the branches and assemble the tree.
public static function makeMenu( $orig_tree ){
$branches = [];
$final_tree = [];
// Assemble branches
foreach( $orig_tree as $node ){
$branches[ $node->parent ][] = $node;
}
$final_tree = $branches[0]; // Set root level manually
unset( $branches[0] );
// Go through the root nodes and see if any branches have them as parents
foreach( $final_tree as &$root_node ){
if( array_key_exists( $root_node->db_id, $branches ) ){
$root_node->children = $branches[ $root_node->db_id ];
unset( $branches[ $root_node->db_id ] );
// Go through the newly added children and see if they have any branches, with them as parents
foreach( $root_node->children as &$child ){
if( array_key_exists( $child->db_id, $branches ) ){
$child->children = $branches[ $child->db_id ];
unset( $branches[ $child->db_id ] );
}
}
}
}
if( !empty( $branches ) ){
// Throw error.
echo 'Something has gone wrong!'; // All branches should have been removed with above code
die();
}
return $final_tree;
}
This works, but it's ugly.
<?php
function makeMenu( $orig_tree ){
$parent_set = [];
$result = [];
foreach($orig_tree as $node){
$node['children'] = [];
if($node['parent'] == 0){
$result[] = $node;
$parent_set[$node['db_id']] = &$result[count($result) - 1];
}else{
$parent = &$parent_set[$node['parent']];
$parent['children'][] = $node;
$parent_set[$node['db_id']] = &$parent['children'][count($parent['children']) - 1];
}
}
return $result;
}
In the above code, as you mentioned parent entries will always come before child entries
We maintain a variable $result where we just add parent level 0 nodes.
Important part is the $parent_set where we store the addresses of nodes present in $result with the help of & address specifier.
Now, whenever we want to add any new node to $result, we fetch it's parent node from $parent_set and add current node in its children array.
Once done, we again update current node's address in $result with the help of parent's children array entry.
Related
I'm currently developing this code that traverse a hierarchical array which should compute the sub-total of a property called cur_compensation. My issue is that the changes I do is not getting save
private function computeSubTotal($hierarchy){
foreach($hierarchy["_children"] as $key => $value){
if(isset($value["_children"]))
{
static::computeSubTotal($value);
}
else{
foreach($hierarchy["_children"] as $employee){
$employee_cur_compensation = $employee["cur_compensation"] ?? 0;
if (!isset($hierarchy["cur_compensation"])) {
$hierarchy["cur_compensation"] = 0;
}
$hierarchy["cur_compensation"] += $employee_cur_compensation;
}
return $hierarchy;
}
}
return $hierarchy;
}
This is the function so what it does it goes to the deepest node, the deepest node is a value that does not have any _children which mean it doesn't have any sub department (the hierarchy is sorted that the sub department are always on top)
The issue I have, once it reaches the bottom it computes the cur_compensation by looping through the employees of that department and adding it on the department "cur_compensation" property.
The issue is that, it doesn't save any of my changes.
So the purpose of the function is to add up the 'cur_compensation' of each employee/sub-department.
For example ->
$rows = array(
array(
'name' => "Main",
'id' => 1,
'parent_id' => 0,
'cur_compensation' => 0,
'_children' => array(
array(
'name' => "Dept A",
'id' => 2,
'parent_id' => 1),
),
array(
'name' => "Dept B",
'id' => 3,
'parent_id' => 1,
'_children' => array(
array(
'name' => "Dept C",
'cur_compensation' => 30000,
'id' => 4,
'parent_id' => 3),
array(
'name' => "Employee C",
'cur_compensation' => 30000,
'id' => 7,
'parent_id' => 3
)
)),
array(
'name' => "Employee A",
'cur_compensation' => 20000,
'id' => 5,
'parent_id' => 1
),
array(
'name' => "Employee B",
'cur_compensation' => 30000,
'id' => 6,
'parent_id' => 1
)
)
)
);
The result I want to get would be:
$rows = array(
array(
'name' => "Main",
'id' => 1,
'parent_id' => 0,
'cur_compensation' => 120000,
'_children' => array(
array(
'name' => "Dept A",
'id' => 2,
'cur_compensation' => 0,
'parent_id' => 1),
),
array(
'name' => "Dept B",
'id' => 3,
'parent_id' => 1,
'cur_compensation' => 60000,
'_children' => array(
array(
'name' => "Dept C",
'cur_compensation' => 30000,
'id' => 4,
'parent_id' => 3),
array(
'name' => "Employee C",
'cur_compensation' => 30000,
'id' => 7,
'parent_id' => 3
)
)),
array(
'name' => "Employee A",
'cur_compensation' => 30000,
'id' => 5,
'parent_id' => 1
),
array(
'name' => "Employee B",
'cur_compensation' => 30000,
'id' => 6,
'parent_id' => 1
)
)
)
);
So you would notice that Main and Dept B got the cur_compensation based on the _children property
There's a few things to make note on here - so I'm going to add comments to your existing code, then provide an example of how you could change it.
(I've formatted the code in each case)
class Example {
// filler code so that we can call
public function process($array){
return $this->computeSubTotal($array);
}
private function computeSubTotal($hierarchy) {
// we're not checking whether "_children" property exists before looping on it
foreach ($hierarchy["_children"] as $key => $value) {
if (isset($value["_children"])) {
// we're calling the method, but not doing anything with the return value.
static::computeSubTotal($value);
// we can set the original array value instead which will provide a modified copy
// this can be resolved by uncommenting the line below
// $hierarchy["_children"][$key] = static::computeSubTotal($value);
// also note that if this "child" doesn't have any *grand*children
// then we won't get an updated value due to how this is structured
// to fix this, you could remove the else wrapping so that the code
// below runs always
} else {
// double looping - we're already looping this array
// this will cause the end value to increase exponentially
foreach ($hierarchy["_children"] as $employee) {
$employee_cur_compensation = $employee["cur_compensation"] ?? 0;
if (!isset($hierarchy["cur_compensation"])) {
$hierarchy["cur_compensation"] = 0;
}
$hierarchy["cur_compensation"] += $employee_cur_compensation;
}
// returning whole array inside the loop is not ideal
// we have already adjusted the main array
// comment out this return to prevent that from happening
return $hierarchy;
}
}
return $hierarchy;
}
}
$example = new Example;
// calling this on $rows won't give us anything back
// since $rows doesn't contain the property "_children"
$rows = $example->process($rows);
// in this case, you would want to process each array result
// only on this primary array
foreach($rows as $index => $value){
$rows[$index] = $example->process($value);
}
echo json_encode($rows, JSON_PRETTY_PRINT);
Taking those comments into account, you would end up with something like this:
private function computeSubTotal($hierarchy) {
// we're not checking whether "_children" property exists before looping on it
foreach ($hierarchy["_children"] as $key => $value) {
if (isset($value["_children"])) {
$hierarchy["_children"][$key] = static::computeSubTotal($value);
}
// double looping - we're already looping this array
// this will cause the end value to increase exponentially
foreach ($hierarchy["_children"] as $employee) {
$employee_cur_compensation = $employee["cur_compensation"] ?? 0;
if (!isset($hierarchy["cur_compensation"])) {
$hierarchy["cur_compensation"] = 0;
}
$hierarchy["cur_compensation"] += $employee_cur_compensation;
}
}
return $hierarchy;
}
That's closer but still, it's not quite correct due to the double looping.
I've made a simpler version that is hopefully easy to follow:
private function computeSubTotal($hierarchy) {
if (!isset($hierarchy["_children"])) {
return $hierarchy;
}
// define this outside the loop for clarity
if (!isset($hierarchy["cur_compensation"])) {
$hierarchy["cur_compensation"] = 0;
}
foreach ($hierarchy["_children"] as $key => $value) {
// don't need to check for "_children" property
// as it's now handled in this function
$updated = static::computeSubTotal($value);
// reference the $updated array to increment
// the "cur_compensation" field
$hierarchy["cur_compensation"] += $updated["cur_compensation"] ?? 0;
// update original array
$hierarchy["_children"][$key] = $updated;
}
return $hierarchy;
}
// call like
foreach ($rows as $index => $value) {
$rows[$index] = static::computeSubTotal($value);
}
You will still need to change how you're passing the $rows variable due to it now containing a "_children" property (as shown in the examples) - either pass each element or add additional logic in that function to handle that.
You need to pass the array as a reference.
https://www.php.net/manual/en/language.references.pass.php
PHP passes the array to the function as a pointer, but when you try to update the array, PHP first makes a full copy of the array and updates the copy instead of the original.
Change your function signature to the following and it should be good.
private function computeSubTotal(&$hierarchy){
P.S. You are calling computeSubTotal statically, but the function is not static itself.
I am having trouble in converting a flat array, e.g. from a DB query, into a tree structure.
I have something like this:
[
[
id => 11,
path => '/11',
label => 'AAA'
],
[
id => 12,
path => '/12',
label => 'BBB'
],
[
id => 21,
path => '/12/21',
label => 'BBBB'
],
[
id => 21,
path => '/12/22',
label => 'CCCCC'
],
]
path points to the hierarchical position inside the tree, defined by the id's. So in the end I have to get something like this:
$tree=[
[
'id' => '11',
'label' => 'AAA'
],
[
'id' => '12',
'label' => 'BBB',
'children' => [
[
'id' => '21',
'label' => 'BBBB'
],
[
'id' => '22',
'label' => 'CCCCC'
]
]
]
];
The depth can be infinite. I would appreciate any solution. Thank you :-)
Thank you all, but I am still stuck :-( Here's my way so far.
First I order the categories by depth & loop them in this order. This way I can be sure that the parent node exists.
public function buildCategoryTree() {
// order by depth
$categoryLevel = [];
foreach (Category::all() as $category) {
$catPathSplitted = array_filter(explode("/", $category->path));
$categoryLevel[count($catPathSplitted)][] = $category;
}
$categoryTree = [];
// now loop each level and create a tree node
foreach ($categoryLevel as $level => $categories) {
foreach ($categories as $category) {
$test = $category->path;
$catPathSplitted = array_filter(explode("/", $category->path));
$categoryTree = $this->addNode($category, $catPathSplitted, $categoryTree, $test);
}
}
}
Then I try to use this recursion, but it only works partially, means I get the child nodes in the hierarchical order,
but the child nodes are also created again on each level. So there's something wrong :-(
private function addNode($cat, $keys, &$tree) {
foreach ($keys as $counter => $id) {
if (!next($keys)) {
if (!isset($tree[$id]['category'])) {
$tree[$id]['category'] = $cat->toArray();
}
} else {
if (!isset($tree[$id]['children'])) {
$tree[$id]['children'] = [];
}
array_shift($keys);
$this->addNode($cat, $keys, $tree[$id]['children'], $test);
}
}
return $tree;
}
Anybody can spot the flaw?
I have blackhole in my mind. Im trying to parse array with multilevel nodes. Here's example array:
global $array;
$array = [
'0' => [
'id' => 1,
'parent' => 0,
'name' => 'root 0'
],
'1' => [
'id' => 2,
'parent' => 1,
'name' => 'root 1'
],
'2' => [
'id' => 3,
'parent' => 2,
'name' => 'root 2'
],
'3' => [
'id' => 4,
'parent' => 3,
'name' => 'root 3'
],
'4' => [
'id' => 5,
'parent' => 3,
'name' => 'root 4'
],
'5' => [
'id' => 6,
'parent' => 2,
'name' => 'root 2'
]
];
This should looks after parse like this. Element 3 with parent 3 should have parent 1, because element 2 has parent 2, and its first child.
I trying to get to this using foreach and function:
global $new_array;
$new_array = [];
foreach( $array as $item )
{
if( $item['parent'] == 0 ) {
$new_array[] = $item; // if parent 0 - clone into new array
continue;
}
//echo $item['name'] . PHP_EOL;
$new_array[] = check_parent( $item['parent'] );
}
print_r($new_array);
function check_parent( $parent )
{
//echo '- check for parent of ' . $parent . PHP_EOL;
global $array;
foreach( $array as $item ) {
if( $item['id'] == $parent && $item['parent'] == 0 ) {
//echo '[OK] found root parent id: ' . $item['id'] . PHP_EOL;
$item['parent'] = $item['id'];
return $item;
} else {
return check_parent( $item['id'] );
}
}
}
I'm so confused, but I didn't see where I make a mistake. Maybe someone, can help me to see - where's problem. I working on it few hours and for now, I had blackhole in my mind.
Fiddle:
https://implode.io/jHS8m1
Desired output:
$new_array = [
'0' => [
'id' => 1,
'parent' => 0,
'name' => 'root 0'
],
'1' => [
'id' => 2,
'parent' => 1,
'name' => 'root 1'
],
'2' => [
'id' => 3,
'parent' => 2, // this should have after parse parent 1
'name' => 'root 2'
],
'3' => [
'id' => 4,
'parent' => 3, // this should have after parse parent 1
'name' => 'root 3'
],
'4' => [
'id' => 5,
'parent' => 3, // this should have after parse parent 1
'name' => 'root 4'
],
'5' => [
'id' => 6,
'parent' => 2, // this should have after parse parent 1
'name' => 'root 2'
]
];
Thanks !
Replace the following line in your code
$new_array[] = check_parent( $item['parent'] ); // get child
with below lines of code.
$temp = check_parent( $item['parent'] ); // get child
$item['parent'] = $temp['id'];
$new_array[] = $item;
What is happening is that your check_parent is returning the $item, which happens to be the parent. However, we are only interested in the id of this. So we get the id and replace the parent it in the original $item.
Here is the working Demo
A bit tardy in my response, but I think it is valuable to provide a refined recursive solution.
My snippet:
Modifies by reference
Does not leverage a global variable declaration
Uses just one loop in the custom recursive function.
Code: (Demo)
function replaceParent(&$array, $parent = null) {
foreach ($array as &$item) {
if ($item['id'] == $parent) {
if ($item['parent']) {
return replaceParent($array, $item['parent']);
} else {
return $item['id'];
}
} elseif ($item['parent']) {
$item['parent'] = replaceParent($array, $item['parent']);
}
}
}
replaceParent($array);
var_export($array);
I'll try to explain...
id 1's parent value of 0 fails both primary conditions, so no recursion/processing is performed on that row of data.
id 2's parent value of 1 passes the elseif condition, so the recursive call goes in search of the row with an id of 1. Finding id 1 with a parent value of 0 means the else branch is satisfied and the id value of 1 is passed back through the recursive call to be assigned to $item['parent'] for the original id 2.
To process id 3 (or deeper), multiple recursive calls occur and all conditions play a role in the search and assignment process. First the elseif leads to the first recursion, then the if's if leads to the second recursion, finally the if's else passes the root id's value all the way back to the original grandchild.
I have a hierarchical array in my project like this:
$Array = array(
array(
'Id' => 1,
'Title' => 'Some Text1',
'Children' => array(
array(
'Id' => 11,
'Title' => 'Some Text11',
'Children' => array(
array(
'Id' => 111,
'Title' => 'Some Text111',
),
array(
'Id' => 112,
'Title' => 'Some Text112',
'Children' => array(
array(
'Id' => 1121,
'Title' => 'Some Text1121',
)
)
)
)
),
array(
'Id' => 12,
'Title' => 'Some Text12',
'Children' => array(
array(
'Id' => 121,
'Title' => 'Some Text121',
)
)
)
)
),
array(
'Id' => 2,
'Title' => 'Some Text2',
)
);
I want to search my string (such as 'Some Text1121') in 'Title' index in this array and return it's path such as after search 'Some Text1121' I want to return this result:
"1 -> 11 -> 112 -> 1121"
Or when I Search 'Some' string, return all path in array.
please help me, thanks.
I've quickly written you something. It's not perfect, but you get the idea:
<?php
function searchRec($haystack, $needle, $pathId = Array(), $pathIndex = Array()) {
foreach($haystack as $index => $item) {
// add the current path to pathId-array
$pathId[] = $item['Id'];
// add the current index to pathIndex-array
$pathIndex[] = $index;
// check if we have a match
if($item['Title'] == $needle) {
// return the match
$returnObject = new stdClass();
// the current item where we have the match
$returnObject->match = $item;
// path of Id's (1, 11, 112, 1121)
$returnObject->pathId = $pathId;
// path of indexes (0,0,1,..) - you might need this to access the item directly
$returnObject->pathIndex = $pathIndex;
return $returnObject;
}
if(isset($item['Children']) && count($item['Children']>0)) {
// if this item has children, we call the same function (recursively)
// again to search inside those children:
$result = searchRec($item['Children'], $needle, $pathId, $pathIndex);
if($result) {
// if that search was successful, return the match-object
return $result;
}
}
}
return false;
}
// useage:
$result = searchRec($Array, "Some Text11");
var_dump($result);
// use
echo implode(" -> ", $result->pathId);
// to get your desired 1 -> 11 -> 112
EDIT: rewritten to make the function actually return something. It now returns an Object with the matching item, the path of Id's and the path of (array-) Indexes.
I have a MySQL database in this format :
table name : btree_mst
fields : id, parent_id, left_node_id, right_node_id, user_name
Now what I have to do is print it in the Un-ordered list format like below
Root Node
Node A
Node A Left
Node A Right
Node B
Node B Left
Node B Right
I tried to make a recursive function for that but didn't work as expected.
Any suggestions ?
Here is the Code I made, http://pastebin.com/X15qAKaA
The only bug in this code is, it is printing UL every time. It should print only when the Level is changed.
Thanks in advance.
If you do not have ordered list in your DB, recursion is suitable.
class A
{
private $a = array(
array(
'id' => 1,
'parent_id' => 0,
'title' => 'ROOT'
),
array(
'id' => 2,
'parent_id' => 1,
'title' => 'A'
),
array(
'id' => 3,
'parent_id' => 1,
'title' => 'B'
),
array(
'id' => 4,
'parent_id' => 2,
'title' => 'A left'
)
);//your database values
public function buildTree()
{
$aNodes = array();
$iRootId = 1;//your root id
foreach ($this->a AS $iK => $aV)
{
if($aV['id'] == $iRootId)
{
unset($this->a[$iK]);
$aNodes[$aV['id']] = $aV;
$aNodes[$aV['id']]['childs'] = $this->getChilds($aV['id']);
}
}
print_r($aNodes);//print tree
}
private function getChilds($iParentId)
{
$aChilds = array();
foreach ($this->a AS $iK => $aV)
{
if($aV['parent_id'] == $iParentId)
{
unset($this->a[$iK]);
$aChilds[$aV['id']] = $aV;
$aChilds[$aV['id']]['childs'] = $this->getChilds($aV['id']);
}
}
return $aChilds;
}
}
$o = new A();
$o->buildTree();