I have a parent/child structure where the it can happen that parent can be deleted, and it's children are still going to be in the database. If that happen, the lowest parent should be set parent of 0.
I'm stuck with this problem because I'm not sure how to structure my (possibly recursive) loop.
I need to return an array of page ID's which parents do not exist; example: array(5, 9, 8);
This is my data set, and the structure can be connected through the parent id; we can see that page ID 8 and 9 have parent of 7 which does not exist:
evar_export($orphans($pages));
$data = array (
0 => array (
'id' => 1,
'url' => 'Home-Page',
'parent' => 0
),
1 => array (
'id' => 2,
'url' => 'page1',
'parent' => 1
),
4 => array (
'id' => 5,
'url' => 'page4',
'parent' => 4
),
5 => array (
'id' => 6,
'url' => 'page5',
'parent' => 5
),
6 => array (
'id' => 8,
'url' => 'no-parent-1',
'parent' => 7
),
7 => array (
'id' => 9,
'url' => 'no-parent-2',
'parent' => 7
)
);
I've tried recursion, but I don't know how to catch the end of the sub-tree:
$orphans = function($array, $temp = array(), $index = 0, $parent = 0, $owner = 0) use(&$orphans) {
foreach ($array as $p) {
if($index == 0) {
$owner = $p['id'];
}
if ($index == 0 || $p['id'] == $parent) {
$temp[] = $p['id'];
$result = $orphans($array, $temp, $index + 1, $p['parent'], $owner);
if (isset($result)) {
return $result;
}
}
else {
return $temp;
}
}
};
I named your data array "pages" for this example:
$orphans = array();
foreach($pages as $p)
{
if($p['parent'] == 0)
continue; //End this iteration and move on.
$id = $p['id'];
$parent = $p['parent'];
$parentExists = false;
foreach($pages as $p2)
{
if( $p2['id'] == $parent )
{
$parentExists = true;
break; //Found, so stop looking.
}
}
if(!$parentExists)
{
$orphans[] = $id;
}
}
If you var_dump the $orphans array after this runs, you would get:
array(2) {
[0]=>
int(8)
[1]=>
int(9)
}
Which appears to be the desired result. Unfortunately nesting another foreach within the foreach is required unless you modify your data structure so the IDs are the keys (which I would advise to reduce resource usage to process this). Using the continue / break control structures at least limits usage.
Clarification on Nested Foreach
An ideal data structure would use key value pairs over sequential items, especially when processing dynamic data, because the keys are unknown. Taking your data for example, getting the 4th item's URL is easy:
$id = $pages[4]['id'];
But there is no relational / logical association between the 4th item and the associated data. Its sequential based on the what ever built the data. If, instead, you assign the id as the key, then we could easily find the parent id of the page with id 4:
$parent = $pages[4]['parent'];
So when doing a simple parse of your data to find non-existing parents, you would just have to do this:
foreach($pages as $p)
{
if($p['parent'] == 0)
continue; //End this iteration and move on.
$id = $p['id'];
if(! isset($pages[$p['parent']])
{
$orphans[] = $id;
}
}
Because then we would know for sure that the key is the id and then logically process the data in that fashion. And considering something like a page id is a primary key (non-duplicate), this should be entirely possible.
But without having a logical association between the key and value in the array, we have to look at the entire data set to find matches for each iteration, causing an exponential explosion of resource usage to complete the task.
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 ==.
SO,
The problem
Suppose we have flat array with following structure:
$array = [
['level'=>1, 'name' => 'Root #1'],
['level'=>1, 'name' => 'Root #2'],
['level'=>2, 'name' => 'subroot 2-1'],
['level'=>3, 'name' => '__subroot 2-1/1'],
['level'=>2, 'name' => 'subroot 2-2'],
['level'=>1, 'name' => 'Root #3']
];
The issue is - transform that array so it will became a tree. Subordination is determined only with elements order and level field. Let's define children as a name of dimension for storing child nodes. For array above that will be:
array (
array (
'level' => 1,
'name' => 'Root #1',
),
array (
'level' => 1,
'name' => 'Root #2',
'children' =>
array (
array (
'level' => 2,
'name' => 'subroot 2-1',
'children' =>
array (
array (
'level' => 3,
'name' => '__subroot 2-1/1',
),
),
),
array (
'level' => 2,
'name' => 'subroot 2-2',
),
),
),
array (
'level' => 1,
'name' => 'Root #3',
),
)
A little more clarifications, if it's not obvious who is parent for who: following code could easily visualize idea:
function visualize($array)
{
foreach($array as $item)
{
echo(str_repeat('-', $item['level']).'['.$item['name'].']'.PHP_EOL);
}
}
visualize($array);
-for array above it's:
-[Root #1]
-[Root #2]
--[subroot 2-1]
---[__subroot 2-1/1]
--[subroot 2-2]
-[Root #3]
Specifics
There are some restrictions both for desired solution and input array:
Input array is always valid: that means it's structure can always be refactored to tree structure. No such weird things as negative/non-numeric levels, no invalid levels structure, e t.c.
Input array can be huge and, currently, maximum level is not restricted
Solution must resolve a matter with single loop, so we can not split array to chunks, apply recursion or jump within array somehow. Just simple foreach (or another loop - it does not matter), only once, each element one-by-one should be handled.
My approach
Currently, I have solution with stack. I'm working with references and maintaining current element of stack to which writing will be done at current step. That is:
function getTree(&$array)
{
$level = 0;
$tree = [];
$stack = [&$tree];
foreach($array as $item)
{
if($item['level']>$level) //expand stack for new items
{
//if there are child elements, add last to stack:
$top = key($stack);
if(count($stack[$top]))
{
end($stack[$top]);
$stack[] = &$stack[$top][key($stack[$top])];
}
//add ['children'] dim to top stack element
end($stack);
$top = key($stack);
$stack[$top]['children'] = [];
$stack[] = &$stack[$top]['children'];
}
while($item['level']<$level--) //pop till certain level
{
//two times: one for last pointer, one for ['children'] dim
array_pop($stack);
array_pop($stack);
}
//add item to stack top:
end($stack);
$stack[key($stack)][] = $item;
$level = $item['level'];
}
return $tree;
}
-since it's long enough, I've created a sample of usage & output.
The question
As you can see, my solution is quite long and it relies on references & array internal pointer handling (such things as end()), so the question is:
May be there are other - shorter and clearer ways to resolve this issue? It looks like some standard question, but I've not found any corresponding solution (there is one similar question - but there OP has exact parent_id subordination while I have not)
The good thing about your problem is that your input is always formatted properly so your actual problem is narrowed down to finding children for each node if they exist or finding parent for each node if it has one. The latter one is more suitable here, because we know that node has parent if its level is more than one and it is the nearest node above it in initial flat array with level that equals level of current node minus one. According to this we can just keep track on few nodes that we are interested in. To be more exact whenever we find two nodes with the same level, the node that was found earlier can't have more children.
Implementation of this will look like this:
function buildTree(array &$nodes) {
$activeNodes = [];
foreach ($nodes as $index => &$node) {
//remove if you don't want empty ['children'] dim for nodes without childs
$node['children'] = [];
$level = $node['level'];
$activeNodes[$level] = &$node;
if ($level > 1) {
$activeNodes[$level - 1]['children'][] = &$node;
unset($nodes[$index]);
}
}
}
Demo
The implementation with using recursion:
function buildTreeHelper(&$array, $currentLevel = 1)
{
$result = array();
$lastIndex = 0;
while($pair = each($array)) {
list(, $row) = $pair;
$level = $row['level'];
if ($level > $currentLevel) {
$result[$lastIndex]['children'] = buildTreeHelper($array, $level);
} else if ($level == $currentLevel) {
$result[++$lastIndex] = $row;
} else {
prev($array); // shift back
break;
}
}
return $result;
}
function buildTree($array)
{
reset($array);
return buildTreeHelper($array);
}
$array = [
['level'=>1, 'name' => 'Root #1'],
['level'=>1, 'name' => 'Root #2'],
['level'=>2, 'name' => 'subroot 2-1'],
['level'=>3, 'name' => '__subroot 2-1/1'],
['level'=>2, 'name' => 'subroot 2-2'],
['level'=>1, 'name' => 'Root #3']
];
print_r(buildTree($array));
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.
I have a data set stored in an array that references itself with parent-child ids:
id, parent_id, title etc. The top tier has a parent_id of 0, and there can be countless parent-child relationships.
So I'm sorting through this array with a foreach loop within a recursive function to check each array element against its parent element, and I think I've been staring at this method too long.
I do end up with the elements in the correct order, but I can't seem to get my lists nested correctly, which makes me think that the method doesn't really work.
Is this the best route to take?
What can I do to improve and fix this method
Is there another trick that I can apply?
Here is my source:
<div>
<div>Subpages</div>
<ul>
<?php subPages($this->subpages->toArray(), 0) ?>
</ul>
<br>
Add New Subpage
</div>
<?php
function subPages($subpages, $parent){
foreach($subpages as $key => &$page){
$newParent = $page['id'];
//If the current page is the parrent start a new list
if($page['id'] == $parent)
{
//Echo out a new list
echo '<ul>';
echo '<li class="collapsed">';
echo '+';
echo ''.$page['title'].'';
subPages($subpages, $newParent);
echo '</li>';
echo '</ul>';
}
//If the page's parent id matches the parent provided
else if($page['parent_id'] == $parent)
{
//Echo out the link
echo '<li class="collapsed">';
echo '+';
echo ''.$page['title'].'';
//Set the page as the new parent
$newParent = $page['id'];
//Remove page from array
unset($subpages[$key]);
//Check the rest of the array for children
subPages($subpages, $newParent);
echo '</li>';
}
}
}
?>
As always, any assistance is appreciated. Please let me know if something isn't clear.
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. I also add a depth marker to each item so that you can format on output.
I will try to help you.
It is possible to compose such relations in one pass:
/**
* Used for "recursive" folding of layout items
* Algorithm of infinite tree (non recursive method)
*
* #param array $items
* #return array
*/
function _foldItems($items) {
$result = array();
foreach ($items as $key => $item) {
$itemName = $item['name'];
if (!isset($item['parent']))
continue;
else {
$parentName = $item['parent']; // it can be either `name` or some `id` of the parent item
if (isset($result[$itemName][$item['sequence']])) {
// Done to eliminate `Warning: Cannot use a scalar value as an array in atLeisure_PropertyImport.class.php`
// Sometimes elements already in the list and have [name] => $count and next line tries to put item in array (item becomes parent)
if ( isset($result[$parentName][$item['parentSequence']]['items'][$itemName]) AND
is_scalar($result[$parentName][$item['parentSequence']]['items'][$itemName])
)
$result[$parentName][$item['parentSequence']]['items'][$itemName] = array();
$result[$parentName][$item['parentSequence']]['items'][$itemName][$item['sequence']] = $result[$itemName][$item['sequence']];
unset($result[$itemName][$item['sequence']]);
} else
$result[$parentName][$item['parentSequence']]['items'][$itemName] = $item['count'];
unset($items[$key]);
} // if //
if (empty($result[$itemName]))
unset($result[$itemName]);
} // foreach //
foreach ($items as $item) { // enumerating rest of the items (single items)
$itemName = $item['itemName'];
if (!isset($result[$itemName]))
$result[$itemName][$item['sequence']] = $item['count'];
}
return $result;
}
Example can be a bit hard to read and to understand because there is really too much code, but I've made this function not so long ago for one project and it seems to be work successfully.
NOTE: It will also work if there are several same items linked to one parent item. It uses item sequence number to avoid aliasing similar values into one.
I have the following code (I know that this code is not optimized but it's not for discussion):
function select_categories($cat_id)
{
$this->db = ORM::factory('category')
->where('parent', '=', $cat_id)
->find_all();
foreach ($this->db as $num => $category)
{
if($category->parent == 0)
{
$this->tmp[$category->parent][$category->id] = array();
}
else {
$this->tmp[$category->parent][$category->id] = array();
}
$this->select_categories($category->id);
}
return $this->tmp;
}
Function returns this array:
array(3) (
0 => array(2) (
1 => array(0)
2 => array(0)
)
2 => array(1) (
3 => array(0)
)
3 => array(2) (
4 => array(0)
5 => array(0)
)
)
But how should I change the code
else {
$this->tmp[$category->parent][$category->id] = array();
// ^^^^^^^^^^^^^^^^^^^^^^ (this bit)
}
To merge array[3] to array[2][3] for example (because array[3] is a subdirectory of array[2] and array[2] is a subdirectory of array[0][2]), so, I need to make this (when I don't know the level of subdirectories):
array (
0 => array (
1 => array
2 => array (
3 => array (
4 => array
5 => array
)
)
)
)
A long time ago I wrote some code to do this in PHP. It takes a list of entities (in your case, categories) and returns a structure where those entities are arranged in a tree. However, it uses associative arrays instead of objects; it assumes that the “parent” ID is stored in one of the associative array entries. I’m sure that you can adapt this to your needs.
function make_tree_structure ($nontree, $parent_field)
{
$parent_to_children = array();
$root_elements = array();
foreach ($nontree as $id => $elem) {
if (array_key_exists ($elem[$parent_field], $nontree))
$parent_to_children [ $elem[$parent_field] ][] = $id;
else
$root_elements[] = $id;
}
$result = array();
while (count ($root_elements)) {
$id = array_shift ($root_elements);
$result [ $id ] = make_tree_structure_recurse ($id, $parent_to_children, $nontree);
}
return $result;
}
function make_tree_structure_recurse ($id, &$parent_to_children, &$nontree)
{
$ret = $nontree [ $id ];
if (array_key_exists ($id, $parent_to_children)) {
$list_of_children = $parent_to_children [ $id ];
unset ($parent_to_children[$id]);
while (count ($list_of_children)) {
$child = array_shift ($list_of_children);
$ret['children'][$child] = make_tree_structure_recurse ($child, $parent_to_children, $nontree);
}
}
return $ret;
}
To see what this does, first try running it on a structure like this:
var $data = array (
0 => array('Name' => 'Kenny'),
1 => array('Name' => 'Lilo', 'Parent' => 0),
2 => array('Name' => 'Adrian', 'Parent' => 1)
3 => array('Name' => 'Mark', 'Parent' => 1)
);
var $tree = make_tree_structure($data, 'Parent');
If I’m not mistaken, you should get something like this out: (the “Parent” key would still be there, but I’m leaving it out for clarity)
array (
0 => array('Name' => 'Kenny', 'children' => array (
1 => array('Name' => 'Lilo', 'children' => array (
2 => array('Name' => 'Adrian')
3 => array('Name' => 'Mark')
)
)
)
Examine the code to see how it does this. Once you understand how this works, you can tweak it to work with your particular data.
Assuming you dont want any data/children tags in your array:
foreach ($this->db as $num => $category)
{
// save the data to the array
$this->tmp[$category->id] = array();
// save a reference to this item in the parent array
$this->tmp[$category->parent][$category->id] = &$this->tmp[$category->id];
$this->select_categories($category->id);
}
// the tree is at index $cat_id
return $this->tmp[$cat_id];
If you just need to retrieve the full tree out of the database, you can even simplify your query (get all records at once) and remove the recursive call in this function. You will need an extra check that will only set the $this->tmp[$catagory->id] when it does not exist and else it should merge the data with the existing data.