Related
Need suggestion on the best way to do a traversal on my hierarchy array (at this point I think it's a tree)
A snippet of my array is this:
$rows = array(
array(
'name' => "Main",
'id' => 1,
'parent_id' => 0,
'_children' => array(
array(
'name' => "Two",
'id' => 2,
'parent_id' => 1),
),
array(
'name' => "Three",
'id' => 3,
'parent_id' => 1,
'_children' => array(
array(
'name' => "Four",
'id' => 4,
'parent_id' => 3),
)),
)
)
);
So on that snippet, a quick explanation is that 'Main' node is root and it has 2 children "Two" and "Three" then "Three" has a child namely "Four".
The actual data is based on department and sub-departments so the nodes goes up to 5 layers.
The _children field for my layering is because I use Tabulator and that's the required hierarchy on what I want to achieve.
I was able to achieve using recursion the department hierarchy, now I need to traverse each department so I can add employees for each department on the same field "_children".
The reason I wasn't able to achieve adding the employees from the start, it's because when I do recursion it overwrites the employee on _children with the departments.
Any suggestion on how I should tackle the traversal?
Edit -
Here is my method that I used for hierarchy:
private function buildHierarchyDepartment(array $elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = static::buildHierarchyDepartment($elements, $element['id']);
if ($children) {
$element['_children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
I'm not too sure how you want to add employees to the array so I've made some assumptions here.
This code will traverse through all elements of an array recursively until it finds an element that matches the parent ID. At this point, it will add the specified item to the _children property of that "parent".
NOTE: this can be simplified if you preferred passing the array by reference. For this example I've set it up so that it doesn't edit the original array (unless of course you overwrite the variable).
function addChild(array $main, array $item, $parent_id) {
foreach ($main as $key => $element) {
if ($parent_id === $element["id"]) {
// create _children element if not exist
if (!isset($element["_children"])) {
$element["_children"] = [];
}
$element["_children"][] = $item;
// specify $main[$key] here so that the changes stick
// outside this foreach loop
$main[$key] = $element;
// item added - break the loop
break;
}
// continue to check others if they have children
if (isset($element["_children"])) {
$element["_children"] = addChild($element["_children"], $item, $parent_id);
// specify $main[$key] here so that the changes stick
// outside this foreach loop
$main[$key] = $element;
}
}
return $main;
}
$employee = [
"id" => 99,
"name" => "Test Employee",
"parent_id" => 4,
];
$new_rows = addChild($rows, $employee, $employee["parent_id"]);
NOTE: this uses a strict comparison for $parent_id === $element["id"] meaning an int won't match a string. You can either convert these values into the same format or change to a non-strict compare ==.
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;
}
}
}
suppose I have table named categories such as:
id parent_id title
1 0 food
2 1 drinks
3 2 juice
4 0 furniture
5 3 tables
now I want to create dropdown menu on laravel such that it recursively displays child category under parent category with proper indentation or - mark as per depth.E.g.:
<select>
<option value="1">food</option>
<option value="2">-drinks</option>
<option value="3">--juice</option>
<option value="4">furniture</option>
<option value="5">-tables</option>
</select>
Above one is static but I want to generate dropdown structure dynamically as like above recursively for any depth of child category from categories table in laravel.
First of all, you could define a getCategories method on your controller. A recursive method. Ideally, you should implement something like this:
...
// utility method to build the categories tree
private function getCategories($parentId = 0)
{
$categories = [];
foreach(Category::where('parent_id', 0)->get() as $category)
{
$categories = [
'item' => $category,
'children' => $this->getCategories($category->id)
];
}
return $categories;
}
...
Right after, you should pass the final array/collection (or whatever you choose) to the view.
return view('my_view', ['categories' => $this->getCategories()])
Finally, you could use a solution similar to this one.
Not the most elegant, but gets the job done:
<?php
$data = [
['id' => 1, 'parent_id' => 0, 'title' => 'food'],
['id' => 2, 'parent_id' => 1, 'title' => 'drinks'],
['id' => 3, 'parent_id' => 2, 'title' => 'juice'],
['id' => 4, 'parent_id' => 0, 'title' => 'furniture'],
['id' => 5, 'parent_id' => 4, 'title' => 'tables']
];
function recursiveElements($data) {
$elements = [];
$tree = [];
foreach ($data as &$element) {
$element['children'] = [];
$id = $element['id'];
$parent_id = $element['parent_id'];
$elements[$id] =& $element;
if (isset($elements[$parent_id])) { $elements[$parent_id]['children'][] =& $element; }
else { $tree[] =& $element; }
}
return $tree;
}
function flattenDown($data, $index=0) {
$elements = [];
foreach($data as $element) {
$elements[] = str_repeat('-', $index) . $element['title'];
if(!empty($element['children'])) $elements = array_merge($elements, flattenDown($element['children'], $index+1));
}
return $elements;
}
$recursiveArray = recursiveElements($data);
$flatten = flattenDown($recursiveArray);
print_r($flatten);
/*
Outputs:
Array
(
[0] => food
[1] => -drinks
[2] => --juice
[3] => furniture
[4] => -tables
)
*/
Run get method on your Category Eloquent model or use query builder to get all the categories.
Then write a function and call it recursively as many times as you need. filter method would be really helpful to work with your categories collection
Something like this should work:
function getCategories($categories, &$result, $parent_id = 0, $depth = 0)
{
//filter only categories under current "parent"
$cats = $categories->filter(function ($item) use ($parent_id) {
return $item->parent_id == $parent_id;
});
//loop through them
foreach ($cats as $cat)
{
//add category. Don't forget the dashes in front. Use ID as index
$result[$cat->id] = str_repeat('-', $depth) . $cat->title;
//go deeper - let's look for "children" of current category
getCategories($categories, $result, $cat->id, $depth + 1);
}
}
//get categories data. In this case it's eloquent.
$categories = Category::get();
//if you don't have the eloquent model you can use DB query builder:
//$categories = DB::table('categories')->select('id', 'parent_id', 'title')->get();
//prepare an empty array for $id => $formattedVal storing
$result = [];
//start by root categories
getCategories($categories, $result);
Didn't test it myself, but the idea should be clear enough. The good thing is you're only executing a single query. The bad thing is you load the whole table into memory at once.
If your table has more columns that you don't need for this algorithm you should specify only the needed ones in your query.
How can i get the ['id'] from all children elements if i pass it an id.
This is my array...
$array = Array
(
'0' => Array
(
'id' => 1,
'parent_id' => 0,
'order_pos' => 0,
'title' => 'Shirts',
'childs' => Array
(
'0' => Array
(
'id' => 2,
'parent_id' => 1,
'order_pos' => 0,
'title' => 'Small Shirts',
)
)
),
'1' => Array
(
'id' => 3,
'parent_id' => 0,
'order_pos' => 0,
'title' => 'Cameras'
)
);
If i write i function and pass a variable of say id 1 can someone please tell me how i can return a single dimensional array with merely just the id's of all child elements.. For instance.
From the previous array, if i pass the id of 1, i want the function to return 1, 2 as 2 is an id element of a child element. So if i pass it 2, it should only return 2 as it doesnt have any children.
I hope you understand me, thank you if you can help me...
Note, this can be unlimited, meaning each parent category can have unlimited sub categories or children.
There is basically two problems you need to solve:
search the entire array for the given ID to start at.
pluck all the IDs from the children once the ID is found.
This would work:
function findIds(array $array, $id)
{
$ids = array();
$iterator = new RecursiveIteratorIterator(
new RecursiveArrayIterator($array),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $val) {
if (is_array($val) && isset($val['id']) && $val['id'] === $id) {
$ids[] = $val['id'];
if (isset($val['childs'])) {
array_walk_recursive(
$val['childs'],
function($val, $key) use (&$ids) {
if ($key === 'id') {
$ids[] = $val;
}
}
);
}
}
}
return $ids;
}
print_r( findIds($array, 1) ); // [1, 2]
print_r( findIds($array, 2) ); // [2]
print_r( findIds($array, 3) ); // [3]
The Iterators will make your array fully traversable. This means, you can foreach over the entire array like it was a flat one. Normally, it would return only the leaves (1, 0, 0, Shirts, …), but since we gave it the SELF_FIRST option it will also return the arrays holding the leaves. Try putting a var_dump inside the foreach to see.
In other words, this
foreach ($iterator as $val) {
will go over each and every value in the array.
if (is_array($val) && isset($val['id']) && $val['id'] === $id) {
This line will only consider the arrays and check for the ID you passed to the findById function. If it exists, the ID is added to the array that will get returned by the function. So that will solve problem 1: finding where to start.
if (isset($val['childs'])) {
If the array has an item "childs" (it should be children btw), it will recursively fetch all the IDs from that item and add them to the returned array:
array_walk_recursive(
$val['childs'],
function($val, $key) use (&$ids) {
if ($key === 'id') {
$ids[] = $val;
}
}
);
The array_walk_recursive accepts an array (1st argument) and will pass the value and the key of the leaves to the callback function (2nd argument). The callback function merely checks if the leaf is an ID value and then add it to the return array. As you can see, we are using a reference to the return array. That is because using use ($ids) would create a copy of the array in the closure scope while we want the real array in order to add items to it. And that would solve problem 2: adding all the child IDs.
So I have an array of items in php, some may be linked to others via a parent_id key. I'm looking to sort this array so that any items whose parent is in this array ends up positioned right below the parent.
example: (actual array has many more keys)
some_array[0]['id'] = 15001;
some_array[0]['parent_id'] = 14899;
some_array[1]['id'] = 14723;
some_array[1]['parent_id'] = 0; //parent_id of 0 means item has no parent of its own
some_array[2]['id'] = 14899;
some_array[2]['parent_id'] = 0;
some_array[3]['id'] = 15000;
some_array[3][parent_id'] = 14723;
I'd like to sort these so they end up in this order:
some_array[0]['id'] = 14723;
some_array[1]['id'] = 15000;
some_array[2]['id'] = 14899;
some_array[3]['id'] = 15001;
ie. items are just below their parents.
Thanks in advance!
My shorter version of mattwang's answer:
/**
* sort parents before children
*
* #param array $objects input objects with attributes 'id' and 'parent'
* #param array $result (optional, reference) internal
* #param integer $parent (optional) internal
* #param integer $depth (optional) internal
* #return array output
*/
function parent_sort(array $objects, array &$result=array(), $parent=0, $depth=0) {
foreach ($objects as $key => $object) {
if ($object->parent == $parent) {
$object->depth = $depth;
array_push($result, $object);
unset($objects[$key]);
parent_sort($objects, $result, $object->id, $depth + 1);
}
}
return $result;
}
Only actual difference is that it sorts an array of objects instead of an array of arrays.
I doubt that you guys are still looking for a real answer to this, but it might help out others with the same problem. Below is a recursive function to resort an array placing children beneath parents.
$initial = array(
array(
'name' => 'People',
'ID' => 2,
'parent' => 0
),
array(
'name' => 'Paul',
'ID' => 4,
'parent' => 2
),
array(
'name' => 'Liz',
'ID' => 5,
'parent' => 2
),
array(
'name' => 'Comus',
'ID' => 6,
'parent' => 3
),
array(
'name' => 'Mai',
'ID' => 7,
'parent' => 2
),
array(
'name' => 'Titus',
'ID' => 8,
'parent' => 3
),
array(
'name' => 'Adult',
'ID' => 9,
'parent' => 6
),
array(
'name' => 'Puppy',
'ID' => 10,
'parent' => 8
),
array(
'name' => 'Programmers',
'ID' => 11,
'parent' => 4
) ,
array(
'name' => 'Animals',
'ID' => 3,
'parent' => 0
)
);
/*---------------------------------
function parentChildSort_r
$idField = The item's ID identifier (required)
$parentField = The item's parent identifier (required)
$els = The array (required)
$parentID = The parent ID for which to sort (internal)
$result = The result set (internal)
$depth = The depth (internal)
----------------------------------*/
function parentChildSort_r($idField, $parentField, $els, $parentID = 0, &$result = array(), &$depth = 0){
foreach ($els as $key => $value):
if ($value[$parentField] == $parentID){
$value['depth'] = $depth;
array_push($result, $value);
unset($els[$key]);
$oldParent = $parentID;
$parentID = $value[$idField];
$depth++;
parentChildSort_r($idField,$parentField, $els, $parentID, $result, $depth);
$parentID = $oldParent;
$depth--;
}
endforeach;
return $result;
}
$result = parentChildSort_r('ID','parent',$initial);
print '<pre>';
print_r($result);
print '</pre>';
It's a wind down method that removes elements from the original array and places them into result set in the proper order. I made it somewhat generic for you, so it just needs you to tell it what your 'ID' field and 'parent' fields are called. Top level items are required to have a parent_id (however you name it) of 0.
You can use usort to sort by a user defined function:
function cmp($a, $b)
{
if ( $a['id'] == $b['id'] ) {
return 0;
} else if ( $a['parent_id'] ) {
if ( $a['parent_id'] == $b['parent_id'] ) {
return ( $a['id'] < $b['id'] ? -1 : 1 );
} else {
return ( $a['parent_id'] >= $b['id'] ? 1 : -1 );
}
} else if ( $b['parent_id'] ) {
return ( $b['parent_id'] >= $a['id'] ? -1 : 1);
} else {
return ( $a['id'] < $b['id'] ? -1 : 1 );
}
}
usort($some_array, "cmp");
Note: this will only work with a tree that is one level deep (meaning no children of children). For more complex trees you probably want to sort the data into a graph and then flatten it.
Edit: fixed to edit a case where $b has a parent but $a does not.
Just use usort() function and compare two different elements of the 'big array' in a way you need. This becomes then a question about 'how do I really decide which element is before which element?'.
The simple usort won't work if you want to support more than one layer of children. There's simply no way to know how two arbitrary elements compare without other information.
I didn't think about it much, so perhaps it doesn't work. But here's a sorting class:
class TopSort
{
private $sorted, $unsorted;
private $history;
public function sort(array $unsorted)
{
$this->sorted = array();
$this->unsorted = $unsorted;
$this->history = array();
usort($this->unsorted, function($a, $b)
{
return $b['id'] - $a['id'];
});
foreach ($this->unsorted as $i => $a)
if ($a['parent_id'] == 0) $this->visit($i);
return array_reverse($this->sorted);
}
private function visit($i)
{
if (!array_key_exists($i, $this->history))
{
$this->history[$i] = true;
foreach ($this->unsorted as $j => $a)
if ($a['parent_id'] == $this->unsorted[$i]['id']) $this->visit($j);
$this->sorted[] = $this->unsorted[$i];
}
}
}
$sorter = new TopSort();
$some_array = $sorter->sort($some_array);
The idea here is to first sort in reverse by id. Then build up a new array by inserting the deepest elements (those with no children) first. Since we initially sorted the array by reverse id, it should mean the entire thing is upside down. After reversing the array, it should be exactly like you want. (Of course one could unshift items onto the array to avoid the reverse operation, but that might be slower...)
And this is very unoptimized as it iterates over the entire array many, many times. With a little rework, it wouldn't need to do that.
Here's an alternative class that is more optimized:
class TopSort
{
private $sorted;
public function sort(array $nodes)
{
$this->sorted = array();
# sort by id
usort($nodes, function($a, $b) {
return $a['id'] - $b['id'];
});
# build tree
$p = array(0 => array());
foreach($nodes as $n)
{
$pid = $n['parent_id'];
$id = $n['id'];
if (!isset($p[$pid]))
$p[$pid] = array('child' => array());
if (isset($p[$id]))
$child = &$p[$id]['child'];
else
$child = array();
$p[$id] = $n;
$p[$id]['child'] = &$child;
unset($child);
$p[$pid]['child'][] = &$p[$id];
}
$nodes = $p['0']['child'];
unset($p);
# flatten array
foreach ($nodes as $node)
$this->flatten($node);
return $this->sorted;
}
private function flatten(array $node)
{
$children = $node['child'];
unset($node['child']);
$this->sorted[] = $node;
foreach ($children as $node)
$this->flatten($node);
}
}
$sorter = new TopSort();
$sorted = $sorter->sort($some_array);
It's a three step approach:
Sort by id (usort)
Build nested array structure.
Flatten array in pre-order.
By virtue of presorting by id, each group of children should be sorted correctly.