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.
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 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 requirement to allow my end users to input formula much like a spreadsheet. I have an array like this:
$table = array(
1=>array(
"id"=>1,
"Name"=>"Regulating",
"Quantity"=>"[2]Quantity+[3]Value",
"Value"=>"[2]Cost"
),
...)
The first level array key is always the same value as the id key in that array.
A tabulated example follows:
id Name Quantity Value
1 Regulating [2]Quantity+[3]Value [2]Cost
2 Kerbs 3 6
3 Bricks 9 7
4 Sausages [3]Cost 3
5 Bamboo [4]Quantity [7]Cost
6 Clams [4]Quantity NULL
7 Hardcore [3]Quantity*0.5 12
8 Beetles [6]Quantity*[4]Value [2]Value
The Quantity and Value keys represent formula which reference the [id] and either Quantity, Value or Cost.
Cost is derived by multiplying the Value and Quantity.
I am using:
preg_match_all("/\[(.*?)\]([A-Z]*[a-z]*)/", $string, $matches, PREG_SET_ORDER);
which outputs an array like so for[1][Quantity]:
Array
(
[0] => Array
(
[0] => [2]Quantity
[1] => 2
[2] => Quantity
)
[1] => Array
(
[0] => [3]Value
[1] => 3
[2] => Value
)
)
Iterating through the table using something similar to:
$calcString = $table[1]['Quantity'];`
foreach ($matches as $match) {
$calcString = str_replace($match[0], $table[$match[1]][$match[2]], $calcString);
}
I can get the string to be calculated and am using a matheval class to do the sum.
For example
[1]Quantity = [2]Quantity + [3]Value
[2]Quantity = 3
[3]Value = 7 // [1]Quantity = 3 + 7 = 10
[1]Value = [2]Cost
[2]Cost = [2]Quantity * [2]Value // 3 * 6 = 18
Basically the variables in the table refer to other [id]key in the same table.
But here is my issue
I need to resolve references to other parts of the table (which may or may not themselves be formula) to fill in the blanks. This is outside my comfort zone and I would appreciate any advice (or even better functional code) which provides enlightenment on how I might be able to achieve this.
Thanks
Deep down, you already know how to solve this, you're just intimidated by the task.
A recursive approach would be to expand references instantly. For example,
expand('[1]Value') # returns '[2]Cost'
expand('[2]Cost') # returns '[2]Quantity * [2]Value'
expand('[2]Quantity') # returns 3
expand('[2]Value') # returns 6
eval('3 * 6')
# returns 18
# returns 18
# returns 18
An iterative (non-recursive) approach is to expand one reference at a time and repeat until there are unresolved references in the string.
expand('[1]Value') // returns '[2]Cost'
expand('[2]Cost') // returns '[2]Quantity + [2]Value'
expand('[2]Quantity + [2]Value') // returns 3 for [2]Quantity
expand('3 * [2]Value') // returns 6 for [2]Value
eval('3 * 6')
# returns 18
Normally, I prefer iterative solutions, because they're much less prone to stack overflows. However, recursive solutions are usually easier to write.
Here's a quickly slapped-together recursive evaluator: https://gist.github.com/stulentsev/b270bce4be67bc1a96ae (written in ruby, though)
If calcString's are reasonably sized and you don't expect replacements to get too elaborate, you could use a while loop to simulate the recursion. Here's an example that outputs the string along the way as it is being modified:
$calcString = $table[8]['Quantity'];
preg_match_all("/\[(.*?)\]([A-Z]*[a-z]*)/", $calcString, $matches, PREG_SET_ORDER);
print_r($calcString . "\n");
while (!empty($matches)){
foreach ($matches as $match) {
preg_match_all("/\[(.*?)\](Cost)/", $match[0], $matchCost, PREG_SET_ORDER);
if (!empty($matchCost)){
$cost = $table[$matchCost[0][1]]['Quantity'] . "*" . $table[$matchCost[0][1]]['Value'];
$calcString = str_replace($match[0], $cost, $calcString);
} else {
$calcString = str_replace($match[0], $table[$match[1]][$match[2]], $calcString);
}
print_r($calcString . "\n");
}
preg_match_all("/\[(.*?)\]([A-Z]*[a-z]*)/", $calcString, $matches, PREG_SET_ORDER);
}
Output:
[6]Quantity*[4]Value
[4]Quantity*[4]Value
[4]Quantity*3
[3]Cost*3
9*7*3
The table variable:
$table = array(
1 => array(
"id" => 1,
"Name" => "Regulating",
"Quantity" => "[2]Quantity+[3]Value",
"Value" => "[2]Cost"
),
2 => array(
"id" => 2,
"Name" => "Kerbs",
"Quantity" => 3,
"Value" => 6
),
3 => array(
"id" => 3,
"Name"=>"Bricks",
"Quantity"=> 9,
"Value"=> 7
),
4 => array(
"id" => 2,
"Name" => "Sausages",
"Quantity" => "[3]Cost",
"Value" => 3
),
5 => array(
"id" => 2,
"Name" => "Bamboo",
"Quantity" => "[4]Quantity",
"Value" => "[7]Cost"
),
6 => array(
"id" => 2,
"Name" => "Clams",
"Quantity" => "[4]Quantity",
"Value" => NULL
),
7 => array(
"id" => 2,
"Name" => "Hardcore",
"Quantity" => "[3]Quantity*0.5",
"Value" => 12
),
8 => array(
"id" => 2,
"Name" => "Beetles",
"Quantity" => "[6]Quantity*[4]Value",
"Value" => "[2]Value"
)
);
A dangerously easy, and your-situation-specific well-performable solution!
<?php
class solver {
private
// The final output array
$arr_evaled,
// When a cell gains its final value, the corresponding entry in the following array gets marked as being done!
$arr_done;
private $solving_iterations_count;
public function solver($array) {
$this->arr_done = array();
foreach($array as $k => $arr)
$this->arr_done[$k] = array('Quantity' => false, 'Value' => false);
// Firstly,expand all of the "[x]Cost"s to "([x]Quantity*[x]Value)"s!
$this->arr_evaled = array_map(
function($v){ return preg_replace('#\[(\d*?)\]Cost#', '([$1]Quantity*[$1]Value)', $v); },
$array
);
$this->solving_iterations_count = 0;
$this->solve();
}
private function isDone() {
foreach($this->arr_done as $a)
if($a['Quantity'] == false || $a['Value'] == false)
return false;
return true;
}
private function isCellDone($id, $fieldName) {
return $this->arr_done[$id][$fieldName];
}
private function markCellAsDone($id, $fieldName, $evaluation) {
$this->arr_done[$id][$fieldName] = true;
$this->arr_evaled[$id][$fieldName] = $evaluation;
}
private function isEvaluable($str) {
return preg_match('#^[0-9*+-\/\(\)\.]*$#', $str) == 1 || strtolower($str)=='null';
}
private function replace($from, $to) {
foreach($this->arr_evaled as &$arr) {
$arr['Quantity'] = str_replace($from, $to, $arr['Quantity']);
$arr['Value'] = str_replace($from, $to, $arr['Value']);
}
}
private function solve() {
$isSolvable = true; // YOUR TODO: I believe coding this part is also fun!) (e.g: check for "reference cycles")
if(!$isSolvable) return null;
while( !$this->isDone() )
{
foreach($this->arr_evaled as $arr) {
foreach(['Quantity', 'Value'] as $fieldName) {
if(!$this->isCellDone($arr['id'], $fieldName)) {
if($this->isEvaluable($arr[$fieldName])) {
$evaluation = eval("return {$arr[$fieldName]};");
$this->markCellAsDone($arr['id'], $fieldName, $evaluation);
$this->replace("[{$arr['id']}]$fieldName", "$evaluation");
}
}
}
}
$this->solving_iterations_count++;
}
foreach($this->arr_evaled as &$row)
$row['Cost'] = $row['Quantity'] * $row['Value'];
return $this->arr_evaled;
}
public function print_tabulated() {
echo "The count of solving iterations: {$this->solving_iterations_count}<br/><br/>";
echo '<table border="1"><tr><th>id</th><th>Name</th><th>Quantity</th><th>Value</th><th>Cost</th></tr>';
foreach($this->arr_evaled as $arr)
echo "<tr><td>{$arr['id']}</td><td>{$arr['Name']}</td><td>{$arr['Quantity']}</td><td>{$arr['Value']}</td><td>{$arr['Cost']}</td></tr>";
echo '</table>';
}
}
// Testing
$arr = array(
1 => array( 'id' => 1, 'Name' => 'Regulating', 'Quantity' => '[2]Quantity+[3]Value', 'Value' => '[2]Cost' ),
2 => array( 'id' => 2, 'Name' => 'Kerbs', 'Quantity' => '3', 'Value' => '6' ),
3 => array( 'id' => 3, 'Name' => 'Bricks', 'Quantity' => '9', 'Value' => '7' ),
4 => array( 'id' => 4, 'Name' => 'Sausages', 'Quantity' => '[3]Cost', 'Value' => '3' ),
5 => array( 'id' => 5, 'Name' => 'Bamboo', 'Quantity' => '[4]Quantity', 'Value' => '[7]Cost' ),
6 => array( 'id' => 6, 'Name' => 'Clams', 'Quantity' => '[4]Quantity', 'Value' => 'NULL' ),
7 => array( 'id' => 7, 'Name' => 'Hardcore', 'Quantity' => '[3]Quantity*0.5', 'Value' => '12' ),
8 => array( 'id' => 8, 'Name' => 'Beetles', 'Quantity' => '[6]Quantity*[4]Value', 'Value' => '[2]Value' ),
);
echo '<pre>';
(new solver($arr))->print_tabulated();
Here is the output:
I'm trying to optimize an e-commerce category system with unlimited category depth (barring system memory limitations). I retrieve all the categories at once and order them as a multi-dimensional array that roughly looks like:
[array] (
[0] (
'CategoryId' => 1,
'ParentCategoryId' => 0,
'Title' => 'Category A',
'SubCategories' => [array] (
[0] (
'CategoryId' => 2,
'ParentCategoryId' => 1,
'Title' => 'Category B',
'SubCategories' => [array] (
[0] (
'CategoryId' => 3,
'ParentCategoryId' => 2,
'Title' => 'Category C'
)
)
)
)
)
)
Each item in the array is actually an object, but for simplicity I wrote it out kind of like an array format.
I'm able to traverse my tree downwards using this function:
/**
* Find Branch using Recursive search by Object Key
* #param String Needle
* #param Array Haystack
* #return Array
*/
public static function findBranchByKey($key, $needle, $haystack)
{
foreach ($haystack as $item)
{
if ( $item->$key == $needle || ( is_object($item) && $item = self::findBranchByKey($key, $needle, $item->SubCategories)) )
{
return $item;
}
}
return false;
}
This finds the object with a matching key and returns it (which may contain more subcategories).
My issue is figuring out how to traverse the other direction. For example, using the data above, let's say I am displaying "Category C" and want to create bread crumbs of it's parents. I can't think of a good way to take my tree array, jump to a specific subcategory, then iterate upwards to get each parent. A resulting array from something like this could be like this so it's easy to spit them out as bread crumbs:
array( 'Category A', 'Category B', 'Category C' )
I could probably do this using SQL in my database but I'd like to retrieve the tree once, cache it, and perform traversal on that object whenever I need to rather than making tons of queries.
TL;DR; How can I traverse upwards in a multidimensional array of categories?
It can be done by recursion.
Let's say, this function should work:
function getPath($id, $tree, &$path = array()) {
foreach ($tree as $item) {
if ($item['CategoryId'] == $id) {
array_push($path, $item['CategoryId']);
return $path;
}
if (!empty($item['SubCategories'])) {
array_push($path, $item['CategoryId']);
if (getPath($id, $item['SubCategories'], $path) === false) {
array_pop($path);
} else {
return $path;
}
}
}
return false;
}
This:
$data = array(
array(
'CategoryId' => 10,
'ParentCategoryId' => 0,
'SubCategories' => array(
array(
'CategoryId' => 12,
'ParentCategoryId' => 1,
'SubCategories' => array()
),
)
),
array(
'CategoryId' => 1,
'ParentCategoryId' => 0,
'SubCategories' => array(
array(
'CategoryId' => 2,
'ParentCategoryId' => 1,
'SubCategories' => array()
),
array(
'CategoryId' => 3,
'ParentCategoryId' => 1,
'SubCategories' => array()
),
array(
'CategoryId' => 4,
'ParentCategoryId' => 1,
'SubCategories' => array(
array(
'CategoryId' => 5,
'ParentCategoryId' => 4,
'SubCategories' => array()
),
)
)
)
)
);
$result = getPath(5, $data);
print_r($result);
will result in:
Array ( [0] => 1 [1] => 4 [2] => 5 )
Well, I am here again dealing with arrays in php. I need your hand to guide me in the right direction. Suppose the following array:
-fruits
--green
---limon
---mango
--red
---apple
-cars
--ferrari
---enzo
----blue
----black
---318
--lamborg
---spider
---gallardo
----gallado-96
-----blue
-----red
-----gallado-98
The - (hyphen) symbol only illustrates the deep level.
Well, I need to build another array (or whatever), because it should be printed as an HTML select as below:
-fruits
--green
---limon
---mango
--red
---apple
-cars
--ferrari
---enzo
----blue
----black
---318
--lamborg
---spider
---gallardo
----gallado-96
-----blue
-----red
-----gallado-98
Looks that for each level element, it should add a space, or hyphen to determinate that it belongs to a particular parent.
EDIT
The have provide an answer provideng my final code. The html select element will display each level as string (repeating the "-" at the begging of the text instead multi-level elements.
Here's a simple recursive function to build a select dropdown given an array. Unfortunately I'm not able to test it, but let me know if it works. Usage would be as follows:
function generateDropdown($array, $level = 1)
{
if ($level == 1)
{
$menu = '<select>';
}
foreach ($array as $a)
{
if (is_array($a))
{
$menu .= generateDropdown($a, $level+1);
}
else
{
$menu .= '<option>'.str_pad('',$level,'-').$a.'</option>'."\n";
}
}
if ($level == 1)
{
$menu = '</select>';
}
return $menu;
}
OK, I got it with the help of #jmgardhn2.
The data
This is my array:
$temp = array(
array(
'name' => 'fruits',
'sons' => array(
array(
'name' => 'green',
'sons' => array(
array(
'name' => 'mango'
),
array(
'name' => 'banana',
)
)
)
)
),
array(
'name' => 'cars',
'sons' => array(
array(
'name' => 'italy',
'sons' => array(
array(
'name' => 'ferrari',
'sons' => array(
array(
'name' => 'red'
),
array(
'name' => 'black'
),
)
),
array(
'name' => 'fiat',
)
)
),
array(
'name' => 'germany',
'sons' => array(
array(
'name' => 'bmw',
)
)
),
)
)
);
Recursive function
Now, the following function will provide an array with items like [level] => [name]:
function createSelect($tree, $items, $level)
{
foreach ($tree as $key)
{
if (is_array($key))
{
$items = createSelect($key, $items, $level + 1);
}
else
{
$items[] = array('level' => $level, 'text' => $key);
}
}
return $items;
}
Calling the funcion
Now, call the function as below:
$items = createSelect($temp, array(), 0);
Output
If you iterate the final $items array it will look like:
1fruits
2green
3mango
3banana
1cars
2italy
3ferrari
4red
4black
3fiat
2germany
3bmw