PHP Recursive Iteration for Nested Array - php

I have following array, the depth of array cannot be known since array can have n childs.
$menu = [
[
'name' => 'home',
'label' => 'Home',
'uri' => '/home',
'order' => 1,
'attributes' => [
'class' => ['home-class', 'home-class-2'],
'id' => ['home-id']
]
], [
'name' => 'about',
'label' => 'About',
'uri' => '/about',
'order' => 2,
'attributes' => [
'class' => [],
'id' => []
],
'child' => [
[
'name' => 'company_profile',
'label' => 'Company Profile',
'uri' => '/company-profile',
'order' => 1,
'attributes' => [
'class' => [],
'id' => []
]
], [
'name' => 'team',
'label' => 'Team',
'uri' => '/team',
'order' => 2,
'attributes' => [
'class' => ['team-class', 'team-class-2'],
'id' => ['team-id']
],
'child' => [
[
'name' => 'management_team',
'label' => 'Management Team',
'uri' => '/management-team',
'order' => 1,
'attributes' => [
'class' => [],
'id' => []
]
],
[
'name' => 'development_team',
'label' => 'Development Team',
'uri' => '/development-team',
'order' => 2,
'attributes' => [
'class' => [],
'id' => []
]
],
]
],
]
], [
'name' => 'services',
'label' => 'Services',
'uri' => '/services',
'order' => 3,
'attributes' => [
'class' => [],
'id' => []
],
'child' => [
[
'name' => 'web_application',
'label' => 'Web Application',
'uri' => '/web-application',
'order' => 1,
'attributes' => [
'class' => [],
'id' => []
]
], [
'name' => 'mobile_application',
'label' => 'Mobile Application',
'uri' => '/mobile-application',
'order' => 2,
'attributes' => [
'class' => [],
'id' => []
]
], [
'name' => 'cms_development',
'label' => 'CMS Development',
'uri' => '/cms-development',
'order' => 3,
'attributes' => [
'class' => [],
'id' => []
]
],
]
]
];
I want to loop this over and pass data to object, for example.
$nav = new Navigation\Menu('main');
foreach ($menu as $item) {
// Parent element
$navItem = new Navigation\Item($item['name']);
$navItem->setLabel($item['label']);
$navItem->setUri($item['uri']);
$nav->addItem($navItem);
if (isset($item['child']) && is_array($item['child'])) {
// First child
foreach ($item['child'] as $child1) {
$childItem1 = new Navigation\Item($child1['name']);
$childItem1->setLabel($child1['label']);
$childItem1->setUri($child1['uri']);
$navItem->addChild($childItem1);
if (isset($child1['child']) && is_array($child1['child'])) {
// Second child
foreach ($child1['child'] as $child2) {
$childItem2 = new Navigation\Item($child2['name']);
$childItem2->setLabel($child2['label']);
$childItem2->setUri($child2['uri']);
$childItem1->addChild($childItem2);
}
}
}
}
}
This works but with a problem. As you see, I am manually looping over each child, I do not want this, what i am looking for is, It must iterate the array recursively allowing to add any number of child with any depth.
I tried array_walk_recursive or custom recursive function without any result. any pointer to solve this is appreciated.
Thanks.

Here is a little recursive script that should run through every if it is an array or an object, each recursion will return the object. Now this would need some editing for your usage. But it should give you a starting point.
function Navigation($item) {
if (is_object($item)) {
foreach (get_object_vars($item) as $property => $value) {
//If item is an object, then run recursively
if (is_array($value) || is_object($value)) {
$item->$property = Navigation($item);
} else {
$navItem->setLabel($item['label']);
$navItem->setUri($item['uri']);
$nav->addItem($navItem);
}
}
return $nav;
} elseif (is_array($item)) {
foreach ($item as $property => $value) {
//If item is an array, then run recursively
if (is_array($value) || is_object($value)) {
$item[$property] = Navigation($item);
} else {
$navItem->setLabel($item['label']);
$navItem->setUri($item['uri']);
$nav->addItem($navItem);
}
}
return $nav;
}
$navItem->setLabel($item['label']);
$navItem->setUri($item['uri']);
$nav->addItem($navItem);
}

Figured it out finally.
Here is how i did it using custom recursive function.
function recursive($menu, &$nav, $child = false, $parent = null)
{
foreach ($menu as $page) {
$navItem = new Navigation\Item($page['name']);
if (false == $child) {
$nav->addItem($navItem);
} else {
$parent->addChild($navItem);
}
if (isset($page['child'])) {
recursive($page['child'], $nav, true, $navItem);
}
}
}
$nav = new Navigation\Menu('main');
recursive($menu, $nav);

Related

Additional attributes in select options

In createForm.php I have code:
$this->add([
'type' => Element\Select::class,
'name' => 'subcategory_id',
'options' =>[
'label' => 'Subcategory',
'empty_option' => 'Select...',
'value_options' => $subcategoriesTable->fetchAllSubcategories(),
],
'attributes' => [
'required' => false,
'class' => 'custom-select',
],
]);
In SubcategoriesTable.php
public function fetchAllSubcategories()
{
$sqlQuery = $this->sql->select()->order('sub_name ASC');
$sqlStmt = $this->sql->prepareStatementForSqlObject($sqlQuery);
$handler = $sqlStmt->execute();
$row = [];
foreach($handler as $tuple){
$row[$tuple['subcategory_id']] = $tuple['sub_name'];
}
return $row;
}
And generated records from my database to my form:
<option value="1">Name1</option>
How I can change my code for making additional attribute like this?
<option value="1" data-chained="parent_name">Name1</option>
parent name value is from another select. Also generated in the same way.
You'll have to make a few changes.
Inside SubcategoriesTable, you'll have to retrieve the parent's name (I used parent_name as key, since we don't know what the exact column's name..):
public function fetchAllSubcategories()
{
$sqlQuery = $this->sql->select()->order('sub_name ASC');
$sqlStmt = $this->sql->prepareStatementForSqlObject($sqlQuery);
$handler = $sqlStmt->execute();
$row = [];
foreach($handler as $tuple){
$row[$tuple['subcategory_id']] = [
'sub_name' => $tuple['sub_name'],
'parent_name' => $tuple['parent_name']
];
}
return $row;
}
Then, in CreateForm:
$valueOptions = [];
foreach($subcategoriesTable->fetchAllSubcategories() as $subcategoryId => $subcategory){
$valueOptions[] = [
'value' => $subcategoryId,
'label' => $subcategory['sub_name'],
'attributes' => [
'data-chained' => $subcategory['parent_name']
]
];
}
$this->add([
'type' => Element\Select::class,
'name' => 'subcategory_id',
'options' =>[
'label' => 'Subcategory',
'empty_option' => 'Select...',
'value_options' => $valueOptions,
],
'attributes' => [
'required' => false,
'class' => 'custom-select',
],
]);
Re-reading your question, I saw that you need this value for another select. I suggest you to add parent's ID instead of parent's name, it would be easier to handle thing.
As exemple:
public function formAction() {
$subcagetoryForm = new Form();
$subcagetoryForm->add([
'type' => Select::class,
'name' => 'subcategory_id',
'options' => [
'label' => 'Subcategory',
'empty_option' => 'Select...',
'value_options' => [
[
'value' => 1,
'label' => 'Name 1',
'attributes' => [
'data-chained' => 'parent 1'
]
],
[
'value' => 2,
'label' => 'Name 2',
'attributes' => [
'data-chained' => 'parent 2'
]
]
],
],
'attributes' => [
'required' => false,
'class' => 'custom-select',
],
]);
return [
'subcagetoryForm' => $subcagetoryForm
];
}
In the view:
<?= $this->formElement($this->subcagetoryForm->get('subcategory_id')); ?>
Result:
[]
However, I'll suggest you to create a custom element to remove all this logic from the form. The custom element will be helpful if you'll need it inside another form. You can take a look at this question

Recursive function returns NULL but I can echo or var_dump() [duplicate]

This question already has answers here:
How to use return inside a recursive function in PHP
(4 answers)
Closed 9 months ago.
I have a nested menu (a sidebar) like this:
$menu = [
'dashboard' => [
'type' => 'item',
'name' => 'bacheca',
'url' => 'home.php',
],
'administration' => [
'type' => 'header',
'name' => 'amministrazione',
'items' => [
'configuration' => [
'type' => 'item',
'name' => 'configurazione',
'url' => 'configuration.php',
],
'admins' => [
'type' => 'item',
'name' => 'amministratori',
'url' => 'admins.php',
],
'activities' => [
'type' => 'item',
'name' => 'attività',
'url' => 'activities.php',
],
],
],
'intranet' => [
'type' => 'header',
'name' => 'intranet',
'items' => [
'chiefs' => [
'type' => 'item',
'name' => 'capiarea',
'url' => 'chiefs.php',
],
'agents' => [
'type' => 'item',
'name' => 'informatori',
'url' => 'agents.php',
],
'samples' => [
'type' => 'tree',
'name' => 'campioni',
'url' => '#',
'submenu' => array_merge(
[
'dummy' => [
'type' => 'item',
'name' => 'dummy',
'url' => 'dummy.php',
],
],
[
'temperature' => [
'type' => 'item',
'name' => 'temperature',
'url' => 'temperature.php',
],
]),
],
'files' => [
'type' => 'item',
'name' => 'riferimenti normativi',
'url' => 'files.php',
],
'documents' => [
'type' => 'item',
'name' => 'documenti',
'url' => 'documents.php',
],
],
],
];
With a recursive function I try to find the matching 'item' type in the menu and return it, for future elaboration (types 'header' and 'tree' are not targeted). Here is the recursive function I wrote:
function item($needle, $haystack, $result = []){
foreach ($haystack as $key => $value){
switch ($value['type']) {
case 'header':
if (isset($value['items'])){
item($needle, $value['items'], $result);
}
break;
case 'tree':
if (isset($value['submenu'])){
item($needle, $value['submenu'], $result);
}
break;
default:
if ($needle == $key){
$result['name'] = $value['name'];
$result['url'] = $value['url'];
return $result;
}
}
}
}
I don't know why, this function returns nothing: if I type var_dump(item('chiefs')); I expect it to return the item with chief key, but I get nothing
The problem lies in the return statement: in the if ($needle == $key){ .. } condition I can echo or var_dump() the matching array, but when I use return $result, it prints NULL
Here is a "live" script to play with: where am I wrong?
https://www.tehplayground.com/Ix0ODne19Acsjvyx
The problem is that
case 'header':
if (isset($value['items'])){
item($needle, $value['items'], $result);
}
break;
case 'tree':
if (isset($value['submenu'])){
item($needle, $value['submenu'], $result);
}
break;
If it goes into either of these and find what you are looking for, you don't return anything.
You need to save the return value of the recursion, if it's not null, then return it, otherwise, continue.
case 'header':
if (isset($value['items'])){
$v = item($needle, $value['items'], $result);
if ($v) return $v;
}
break;
case 'tree':
if (isset($value['submenu'])){
$v = item($needle, $value['submenu'], $result);
if ($v) return $v;
}
break;

Removing values from multidimensional array based on multidimentional whitelist

I have the following white list:
private $conditionalFieldsWhitelist = [
'Basic Fields' => [
'contact_name',
'contact_lastname',
],
'Required Fields' => [
'some_required_field',
]
];
That I want to run agains an array that looks like this:
$myArray = [
'Basic Fields' => [
[
'field_name' => 'contact_name',
'field_readable' => $this->language->get('First Name'),
'field_type' => 'string',
'field_required' => 'no',
'field_placeholder' => $this->language->get('Type your first name')
], [
'field_name' => 'contact_lastname',
'field_readable' => $this->language->get('Last Name'),
'field_type' => 'string',
'field_required' => 'no',
'field_placeholder' => $this->language->get('Type your last name')
], [
'field_name' => 'contact_email',
'field_readable' => $this->language->get('Email Address'),
'field_type' => 'string',
'field_required' => 'yes',
'field_placeholder' => 'example#domain.com'
], [
'field_name' => 'contact_mobile',
'field_readable' => $this->language->get('Mobile Number'),
'field_type' => 'string',
'field_required' => 'yes',
'field_placeholder' => '+27881234567'
]
],
'Required Fields' => [
[
'field_name' => 'some_required_field',
'field_readable' => $this->language->get('Required Field'),
'field_type' => 'string',
'field_required' => 'no',
],
],
'This Should Be Removed' => [
[
'field_name' => 'not_needed_field',
'field_readable' => $this->language->get('Required Field'),
'field_type' => 'string',
'field_required' => 'no',
]
]
];
Obviously this are watered down versions of the actual arrays.
My code to do this looks like so:
public function getConditionalFields()
{
$conditionalFields = $this->formFieldGroupingsViewHelper->getGroupedFields();
foreach ($conditionalFields as $group => $fields) {
if (in_array($group, array_keys($this->conditionalFieldsWhitelist)) === false) {
unset($conditionalFields[$group]);
continue;
}
foreach ($fields as $index => $field) {
if (in_array($field['field_name'], $this->conditionalFieldsWhitelist[$group]) === false) {
unset($conditionalFields[$group][$index]);
continue;
}
}
$conditionalFields[$group] = array_values($conditionalFields[$group]);
}
return $conditionalFields;
}
This, however, doesn't seem like it's very clean and makes use of the power of PHP.
Is there a simpler, better, neater way to do this? Something like a recursive array_intersect that works on array keys as well.
This is the outcome that I expect:
[
'Basic Fields' => [
[
'field_name' => 'contact_name',
'field_readable' => $this->language->get('First Name'),
'field_type' => 'string',
'field_required' => 'no',
'field_placeholder' => $this->language->get('Type your first name')
], [
'field_name' => 'contact_lastname',
'field_readable' => $this->language->get('Last Name'),
'field_type' => 'string',
'field_required' => 'no',
'field_placeholder' => $this->language->get('Type your last name')
]
],
'Required Fields' => [
[
'field_name' => 'some_required_field',
'field_readable' => $this->language->get('Required Field'),
'field_type' => 'string',
'field_required' => 'no',
],
]
]
I don't know if this is "better". It is different.
Code: (Demo)
$whitegroups = array_keys($conditionalFieldsWhitelist); // only necessary if multiple groups are possible in your project
foreach ($myArray as $group => $field_sets) {
if (in_array($group, $whitegroups)) {
$whitesubsets = $conditionalFieldsWhitelist[$group]; // only filter subsets that pass the group check
$result[$group] = array_filter($field_sets, function($set) use ($whitesubsets) {
return in_array($set['field_name'],$whitesubsets); // filter the subsets
});
}
}
var_export($result);
Or: (with array_intersect_key())
$myArray = array_intersect_key($myArray, $conditionalFieldsWhitelist); // filter the groups/keys
foreach ($myArray as $group => $field_sets) {
$whitesubsets = $conditionalFieldsWhitelist[$group];
$result[$group] = array_filter($field_sets, function($set) use ($whitesubsets) {
return in_array($set['field_name'],$whitesubsets); // filter the subsets
});
}
var_export($result);
Or (with array_intersect_key() and modifying by reference):
$myArray = array_intersect_key($myArray, $conditionalFieldsWhitelist);
foreach ($myArray as $group => &$field_sets) { // modify by reference
$whitesubsets = $conditionalFieldsWhitelist[$group];
$field_sets = array_filter($field_sets, function($set) use ($whitesubsets) {
return in_array($set['field_name'],$whitesubsets);
});
}
var_export($myArray);
Output:
array (
'Basic Fields' =>
array (
0 =>
array (
'field_name' => 'contact_name',
'field_type' => 'string',
'field_required' => 'no',
),
1 =>
array (
'field_name' => 'contact_lastname',
'field_type' => 'string',
'field_required' => 'no',
),
),
)

How to perform Recursion to Nth level?

Here is my single database structure.
I have one array which I will export in one file by var_export.
This is a sample array,
return [
'menus' => [
'products' => [
'name' => 'Products',
'link' => 'cctv.html',
'show' => true,
],
'companies' => [
'name' => 'Companies',
'link' => 'companies.html',
'show' => true,
],
],
'products' => [
'section_enabled' => [
'featured_products', 'categories', 'news', 'latest_products', 'case_studies', 'expert_commentary',
'videos', 'companies', 'topics',
],
'pages' => [
'child_category_page' => [
'middle' => [
'left' => [
'class' => 'col-md-9',
'sections' => [
[
'folder' => '',
'element' => 'page_heading',
'variables' => ['page_heading', 'articles'],
],
],
],
],
],
'product_profile_page' => [
'middle' => [
'left' => [
'class' => 'col-md-9',
'sections' => [
[
'folder' => '',
'element' => 'page_heading',
'variables' => ['page_heading'],
],
[
'folder' => 'products',
'element' => 'specifications',
'variables' => ['specifications', 'filters'],
],
],
],
],
],
],
],
'news' => [
'news_types' => [
'expert_commentary' => 'Etary',
'applications' => 'Mon',
'security_beat' => 'SB',
'round_table' => 'RT',
'case_studies' => [
'type' => 'Marcation',
'label' => 'Casies',
],
],
'url_lookup_values' => [
'caudies' => [
'value' => 'Marklication',
'config' => 'castudies',
],
],
],
];
I got the recursion trick from here.
But it doesn't solve my problem.
Here is what I have tried,
function generate_site_config()
{
$data = DB::table("SITE_CONFIGS")->where("parent_id", 0)->get();
$desired_array = [];
get_tree_site_configs($data,$desired_array,0,0);
// file_put_contents(config_path() . DIRECTORY_SEPARATOR . "frontend_configs_demo.php", '<?php return ' . var_export($data, true) . ';');
echo "success";die;
}
function get_tree_site_configs($inputArray, &$outputArray, $parent = 0, $level = 0){
foreach($inputArray as $cc_id => $sub_arr){
if($sub_arr->parent_id == $parent){
$outputArray[] = $sub_arr;
if($sub_arr->variable_value == ''){
$inputArray = DB::table("SITE_CONFIGS")->where("parent_id", $sub_arr->id)->get();
get_tree_site_configs($inputArray, $outputArray, $sub_arr->id, $level + 1);
}else{
pr($sub_arr);
pr($outputArray);die;
}
}
}
}
NOTE: chain will go until variable_value == '', if variable_value found then it stops the tree at that end, then it will look for other parents which are dangling.

PHP push a value to an array during recursive search

$menus = [
0 => [
'id' => 'home',
'title' => 'Home',
'url' => '/display/home',
'children' => [],
'parent' => null
],
1 => [
'id' => 'nodes',
'title' => 'Nodes',
'url' => 'nodes/index',
'children' => [
0 => [
'id' => 'addNode',
'title' => 'Add Node',
'url' => '/nodes/add',
'children' => [],
'parent' => "nodes"
],
1 => [
'id' => 'editNode',
'title' => 'Edit Node',
'url' => '/nodes/edit',
'children' => [],
'parent' => 'nodes'
],
2 => [
'id' => 'deleteNode',
'title' => 'Delete Node',
'url' => '/nodes/delete',
'children' => [
0 => [
'id' => 'deleteMultipleNodes',
'title' => 'Delete Multiple Nodes',
'url' => '/nodes/deleteall',
'children' => [
0 => [
'id' => 'deleteMultipleSelectedNodes',
'title' => 'Delete Multiple Selected Nodes',
'url' => '/nodes/deleteallselected',
'children' => [],
'parent' => 'deleteMultipleNodes'
]
],
'parent' => 'deleteNode'
]
],
'parent' => 'nodes'
]
],
'parent' => null
]
];
Assuming I have this array. What i want is to recursively search this array for an "id" and if found push a new children to the children array of that element.
I've tried it via different ways, I've also tried to use RecursiveArrayIterator to traverse the array, but the problem is how can i push value to that index of the array when found while traversing.
For Example here is a code from one of my tries:
private function traverseArray($array)
{
$child = [
'id' => 'deleteMultipleNotSelectedNodes',
'title' => 'Delete Multiple Not Selected Nodes',
'url' => '/nodes/deletenotselected',
'children' => [],
'parent' => 'deleteMultipleNodes'
];
foreach($array as $key=>$value)
{
if(is_array($value))
{
$this->traverseArray($value);
}
if($key == "id" && $value == "deleteMultipleNodes")
{
array_push($array["children"], $child); // This part is confusing me, How to add the child on this index where the id is found.
}
}
}
Any help on how to do such thing in an efficient way would save my days.
Here it's how it would work without using $this and fixing bugs in comparing $value instead assigning anything to value.
Please note the difference with &$array and &$value, which are references, so it would change the original data instead of copying it into new variables.
<?php
$menus = [
0 => [
'id' => 'home',
'title' => 'Home',
'url' => '/display/home',
'children' => [],
'parent' => null
],
1 => [
'id' => 'nodes',
'title' => 'Nodes',
'url' => 'nodes/index',
'children' => [
0 => [
'id' => 'addNode',
'title' => 'Add Node',
'url' => '/nodes/add',
'children' => [],
'parent' => "nodes"
],
1 => [
'id' => 'editNode',
'title' => 'Edit Node',
'url' => '/nodes/edit',
'children' => [],
'parent' => 'nodes'
],
2 => [
'id' => 'deleteNode',
'title' => 'Delete Node',
'url' => '/nodes/delete',
'children' => [
0 => [
'id' => 'deleteMultipleNodes',
'title' => 'Delete Multiple Nodes',
'url' => '/nodes/deleteall',
'children' => [
0 => [
'id' => 'deleteMultipleSelectedNodes',
'title' => 'Delete Multiple Selected Nodes',
'url' => '/nodes/deleteallselected',
'children' => [],
'parent' => 'deleteMultipleNodes'
]
],
'parent' => 'deleteNode'
]
],
'parent' => 'nodes'
]
],
'parent' => null
]
];
function traverseArray(&$array)
{
$child = [
'id' => 'deleteMultipleNotSelectedNodes',
'title' => 'Delete Multiple Not Selected Nodes',
'url' => '/nodes/deletenotselected',
'children' => [],
'parent' => 'deleteMultipleNodes'
];
foreach($array as $key=>&$value)
{
if(is_array($value))
{
traverseArray($value);
}
if($key == "id" && $value == "deleteMultipleNodes")
{
array_push($array["children"], $child);
}
}
}
echo "=== before \n";
var_export($menus);
echo "\n\n";
traverseArray($menus);
echo "=== after \n";
var_export($menus);

Categories