Recursive functions and multidimensional arrays - php

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.

Related

Best way to do a traversal on a hierarchy array

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 ==.

Get most deeply nested arrays

I have heterogenous nested arrays (each contains a mix of scalars and arrays, which also may contain scalars and arrays, and so on recursively). The goal is to extract all arrays with the maximum depth. Note this does not mean extracting arrays at the "bottom" of any given sub-array (local maximums), but the greatest depth over all sub-arrays.
For example:
$testArray= array(
'test1' => 'SingleValue1',
'test2' => 'SingleValue2',
'test3' => array(0,1,2),
'test4' => array(array(3,4,array(5,6,7)), array(8,9,array(10,11,12)),13,14),
'test5' => array(15,16,17, array(18,19,20)),
);
In this example, the greatest depth any array occurs at is 3, and there are two arrays at that depth:
array(5,6,7)
array(10,11,12)
The code should find these two. (The [18,19,20] sub-array is not included, for though it's at the greatest depth in its branch, it's at a lesser depth overall.)
I'm not sure where to start. I've tried many things: using foreach in recursive functions, etc., but the end result was always nothing, all elements or the last iterated element. How can this problem be approached? Complete solutions aren't needed, just hints on where to start.
Extended solution with RecursiveIteratorIterator class:
$testArray= array(
'test1' => 'SingleValue1',
'test2' => 'SingleValue2',
'test3' => array(0,1,2),
'test4' => array(array(3,4,array(5,6,7)), array(8,9,array(10,11,12)),13,14),
'test5' => array(15,16,17, array(18,19,20)),
);
$it = new \RecursiveArrayIterator($testArray);
$it = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
$max_depth = 0;
$items = $deepmost = [];
foreach ($it as $item) {
$depth = $it->getDepth(); // current subiterator depth
if ($depth > $max_depth) { // determining max depth
$max_depth = $depth;
$items = [];
}
if (is_array($item)) {
$items[$depth][] = $item;
}
}
if ($items) {
$max_key = max(array_keys($items)); // get max key pointing to the max depth
$deepmost = $items[$max_key];
unset($items);
}
print_r($deepmost);
The output:
Array
(
[0] => Array
(
[0] => 5
[1] => 6
[2] => 7
)
[1] => Array
(
[0] => 10
[1] => 11
[2] => 12
)
)
You may wrap this approach into a named function and use it for getting the deepmost arrays.
Enjoy! )
Roman's solution seems to work, but I struggle to read that type of method. Here's my version of finding the deepest subarrays.
See my inline comments for explanation of each step. Basically, it checks each array for subarrays, then iterates/recurses when possible, and storea subarrays using the level counter as a key.
My custom function will return an array of arrays.
Code: (Multidimensional Array Demo) (Flat Array Demo) (Empty Array Demo)
function deepestArrays(array $array, int $level = 0, array &$lowest = []): array
{
$subarrays = array_filter($array, 'is_array');
if ($subarrays) { // a deeper level exists
foreach ($subarrays as $subarray) {
deepestArrays($subarray, $level + 1, $lowest); // recurse each subarray
}
} else { // deepest level in branch
$lowestLevel = key($lowest) ?? $level; // if lowest array is empty, key will be null, fallback to $level value
if ($lowestLevel === $level) {
$lowest[$level][] = $array; // push the array into the results
} elseif ($lowestLevel < $level) {
$lowest = [$level => [$array]]; // overwrite with new lowest array
}
}
return current($lowest); // return the deepest array
}
var_export(
deepestArrays($testArray)
);

How to find pages without parent page?

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.

Get key level depth in a multi-dimensional array?

I'm trying to search through a multi-dimensional array to return the depth/value and whether it exists, but i'm having a little bit of trouble..
Theres a number of depths/dimensions of my array.. I'm storing current multi-dimensional arrays within others.. Here is an example:
array(
"UserInformation" => array(
array (
"Username" => "Test_User",
"Warnings" => 0,
"Post_ID" => array (7726,2254)
),
array (
"Username" => "Another",
"Warnings" => 2,
"WarningID" => array(8874,1125),
"Post_ID" => array (7726,2254)
),
),
"Mani" => 0,
"Aut" => 1,
"Wn" => 0,
"RTV"=> array(
"RunTime"=> "kk",
"Run_2" => "e",
"Perm"=>"p",
"DEp"=>"d")
);
Now, How would I go about searching the entire array index without nested for or foreach loops?
I've tried performing an array_search but this returns no aval as it only searches through the first dimension, and not in more depth?
You should try this one.
function recursive_array_search($needle,$haystack) {
foreach($haystack as $key=>$value) {
$current_key=$key;
if($needle===$value OR (is_array($value) && recursive_array_search($needle,$value))) {
return $current_key;
}
}
return false;
}
Here OR is for checking whether the needle value is same with the value you are searching. If it is true it will directly return the key and if it is not true it will first check the value is an array then call the same function recursively by changing its input with new nested array. So like this way it will iterate recursively end level of the array to find the value.
Amar's modified code which worked for me:
/*
* recusrive array search
* #param array $array
* #param string $needle
* #return string|int:
*/
public function recursiveFind(array $array, $needle)
{
$iterator = new RecursiveIteratorIterator(
new RecursiveArrayIterator($array),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $key => $item) {
$current_key = $key;
if (is_array($item) && $key === $needle) {
return $current_key;
}
}
}
in this code case I needed ID (int) or keep looking in array (string) until I reach the one

PHP array unset string

I am trying to unset a group of array keys that have the same prefix. I can't seem to get this to work.
foreach ($array as $key => $value) {
unset($array['prefix_' . $key]);
}
How can I get unset to see ['prefix_' . $key] as the actual variable? Thanks
UPDATE: The $array keys will have two keys with the same name. Just one will have the prefix and there are about 5 keys with prefixed keys:
Array {
[name] => name
[prefix_name] => other name
}
I don't want to remove [name] just [prefix_name] from the array.
This works:
$array = array(
'aa' => 'other value aa',
'prefix_aa' => 'value aa',
'bb' => 'other value bb',
'prefix_bb' => 'value bb'
);
$prefix = 'prefix_';
foreach ($array as $key => $value) {
if (substr($key, 0, strlen($prefix)) == $prefix) {
unset($array[$key]);
}
}
If you copy/paste this code at a site like http://writecodeonline.com/php/, you can see for yourself that it works.
You can't use a foreach because it's only a copy of the collection. You'd need to use a for or grab the keys separately and separate your processing from the array you want to manipulate. Something like:
foreach (array_keys($array) as $keyName){
if (strncmp($keyName,'prefix_',7) === 0){
unset($array[$keyName]);
}
}
You're also already iterating over the collection getting every key. Unless you had:
$array = array(
'foo' => 1,
'prefix_foo' => 1
);
(Where every key also has a matching key with "prefix_" in front of it) you'll run in to trouble.
I'm not sure I understand your question, but if you are trying to unset all the keys with a specific prefix, you can iterate through the array and just unset the ones that match the prefix.
Something like:
<?php
foreach ($array as $key => $value) { // loop through keys
if (preg_match('/^prefix_/', $key)) { // if the key stars with 'prefix_'
unset($array[$key]); // unset it
}
}
You're looping over the array keys already, so if you've got
$array = (
'prefix_a' => 'b',
'prefix_c' => 'd'
etc...
)
Then $keys will be prefix_a, prefix_c, etc... What you're doing is generating an entirely NEW key, which'd be prefix_prefix_a, prefix_prefix_c, etc...
Unless you're doing something more complicated, you could just replace the whole loop with
$array = array();
I believe this should work:
foreach ($array as $key => $value) {
unset($array['prefix_' . str_replace('prefix_', '', $key]);
}

Categories