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.
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 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.
I'm trying to print list of categories with indefinite subcategories.
Example:
[
[
'categoryName' => 'Category1',
'categoryUrl' => 'category-1',
'subcategories' => [
[
'categoryName' => 'Subcategory 1',
'categoryUrl' => 'sucbategory-1',
'subcategories' => [
[
'categoryName' => 'Subcategory subcategory 1',
'categoryUrl' => 'sucbategory-subcategory-1',
'subcategories' => [
[
'....'
]
]
]
],
[
'categoryName' => 'Subcategory 2',
'categoryUrl' => 'sucbategory-12',
]
]
]
]
]
I was trying it with foreach inside foreach, etc...
Then I realised I don't know how many levels category tree will have.
Category1->Subcategory1->Subcategory Subcategory1-> Subcategory ... 1-> ??
This is called a recursion. The idea is like this:
function printLeafs($node){
echo $node->title;
$leafs = getLeafs($node);
foreach ($leafs as $leaf){
printLeafs($leaf);
}
}
This will print all using recursion.
function recurse($array) {
foreach( $array as $one ) {
echo $one['categoryName'] . '->' ;
echo $one['categoryUrl'] . '->' ;
if( isset($one['subcategories']) ) {
if( is_array($one['subcategories'])) {
recurse($one['subcategories']) ;
}
}
}
};
recurse($array);
But in your code I've noted a problem, the following looks misplaced. In this case you are having categoryName not having a parent subcategories. If that a typo the above will work. Otherwise it wont.
'categoryName' => 'Subcategory 2',
'categoryUrl' => 'sucbategory-12',
Im trying to list categories with sub categories in my app - I can use either PHP or Javascript / Jquery for the following:
I have an array of categories with sub categories appended as additional arrays
the trick is that it can go as deep as there are sub categories.
and sub categories can also have sub categories.
Therefore for each category it can have as many children each of whom can have many children arrays.
What would be the best way to loop through them to create a dropdown list?
Here is the structure when dumping the main array:
array (size=2)
0 =>
array (size=4)
'id' => int 1
'name' => string 'Stationery' (length=10)
'parent_id' => int 0
'childs' =>
array (size=1)
0 =>
array (size=4)
...
1 =>
array (size=3)
'id' => int 4
'name' => string 'boots' (length=5)
'parent_id' => int 0
notice sub zero has a "childs" array
when dumping this array i get:
array (size=1)
0 =>
array (size=4)
'id' => int 2
'name' => string 'pens' (length=4)
'parent_id' => int 1
'childs' =>
array (size=1)
0 =>
array (size=4)
...
Notice this too has a child attached which when dumped looks like:
array (size=1)
0 =>
array (size=4)
'id' => int 3
'name' => string 'penfillers' (length=10)
'parent_id' => int 2
'childs' =>
array (size=1)
0 =>
array (size=3)
...
Sneaky - this one also has another child!
This can go as deep as there are sub categories
How would i loop through them and have the output in a dropdown list?
Im stumped as to how to loop infinitely until the chain ends.
Thanks
Jason
You should recursively yield all the options in the array. There are 2 ways to implement it. Depends on your PHP version.
To make the core logic cleaner, let's say we'd render the output with these utilities:
//
// some function to tidy up outputs
//
// simply make the recursive level visible
function pad_level($string, $level) {
// no pad for 0 level
if ($level == 0) return $string;
// pad some '-' to show levels
$pad = str_repeat('-', $level);
return $pad . ' ' . $string;
}
// render a leaf into standardized info array for an option
function option_from($item, $level) {
return array(
'value' => $item['id'],
'display_value' => pad_level($item['name'], $level),
);
}
// render options into HTML select
function render_select($name, $options) {
$output = '';
foreach ($options as $option) {
$output .= ' '.
'<option value="'.htmlspecialchars($option["value"]).'">'.
htmlspecialchars($option["display_value"]).
'</option>'."\n";
}
return '<select name="'.htmlspecialchars($name).'">'."\n".
$output."</select>\n";
}
// render options into plain text display
function render_plain_text($name, $options) {
$output = '';
foreach ($options as $option) {
$output .= $option["value"].' => '.$option["display_value"]."\n";
}
return $output;
}
These are the 2 methods:
//
// core logic
//
// Method 1: Generator. Available with PHP 5 >= 5.5.0
function options_in($array, $level=0) {
foreach ($array as $leaf) {
yield option_from($leaf, $level);
// yield the children options, if any
if (isset($leaf['childs']) && is_array($leaf['childs'])) {
foreach (options_in($leaf['childs'], $level+1) as $option) {
yield $option;
}
}
}
}
// Method 2: Normal recursion then merge arrays. For PHP 4 or after
function get_options($array, $level=0) {
$output = array();
// yield for the option array
foreach ($array as $leaf) {
$output[] = option_from($leaf, $level);
// yield the children
if (isset($leaf['childs']) && is_array($leaf['childs'])) {
$childs = get_options($leaf['childs'], $level+1);
$output = array_merge($output, $childs); // this could be slow
}
}
return $output;
}
And this is how you actually render some HTML from it:
// dummy input
$input = array(
array(
'id' => 1,
'name' => 'Stationery',
'parent_id' => 0,
'childs' => array(
array(
'id' => 2,
'name' => 'Pencil',
'parent_id' => 1,
),
array(
'id' => 3,
'name' => 'Pen',
'parent_id' => 1,
),
array(
'id' => 5,
'name' => 'Notepad',
'parent_id' => 1,
'childs' => array(
array(
'id' => 8,
'name' => 'Blue Pad',
'parent_id' => 3,
),
array(
'id' => 9,
'name' => 'Red Pad',
'parent_id' => 3,
),
array(
'id' => 10,
'name' => 'iPad',
'parent_id' => 3,
),
),
),
),
),
array(
'id' => 4,
'name' => 'boots',
'parent_id' => 0,
),
);
// method 1, preferred
echo "\nMethod 1\n";
echo render_select('mySelect', options_in($input));
echo render_plain_text('mySelect', options_in($input));
// method 2
echo "\nMethod 2\n";
echo render_select('mySelect', get_options($input));
echo render_plain_text('mySelect', get_options($input));
Here is a really simple example of how you could do it using recursion. I'm sure there is better ways but this is a very simple function so you can see the concept. The function calls itself until the job is done (I'm using the square bracket array notation here so make sure your PHP version supports it)
<?php
function getItems(array $items)
{
$return = [];
foreach ($items as $item) {
$return[] = $item;
if (isset($item['childs']) && count($item['childs']) > 0) {
$return = array_merge(
$return,
getItems($item['childs'])
);
}
}
return $return;
}
$array = [
0 => [
'id' => 1,
'childs' => []
],
1 => [
'id' => 2,
'childs' => [
0 => [
'id' => 3,
'childs' => []
]
]
]
];
print_r(getItems($array));
Then just loop over the results to create your select options. Hope this helps
create a sub function, input of the sub function would be object ,this function will check if it is an array or a simple element, if it is an array then it will call the function again on that element else return the element.
just elaborate "Gerald Schneider"'s answer.
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();