Title: How to take a flat JSON file and build a hierarchical menu using PHP
Tags: php;json;menu
Question: I have a database with a list of menu links. Every menu link has an ID in the field 'menu_pages_id'.
In the database there is a field &quot;subPageOf&quot; which says what its parent is. There are also three different kinds of menu links, which is defined in the field &quot;menuType&quot;
I have outputted this list into a flat JSON file, and want to reconstruct the hierarchy.
$jsonMenu = json_decode($jsonMenu);
foreach ($jsonMenu as $pageInfo ) {
$pageUrl = $pageInfo-&gt;pageUrl;
$menuPageName = $pageInfo-&gt;pageName;
$menuType = $pageInfo-&gt;menuType;
$subPageOf = $pageInfo-&gt;subPageOf;
$menu_pages_id = $pageInfo-&gt;menu_pages_id;
if( $menuType == 'header' &amp;&amp;  $subPageOf == '0' ){
$headerMenu .= &quot;<a href='$pageUrl'&gt;$menuPageName</a&gt;&quot;;
if( $menuType == 'category' &amp;&amp;  $subPageOf == '0'){
$categoryMenu .= &quot;<li&gt;<a href='$pageUrl'&gt;$menuPageName</a&gt;</li&gt;&quot;;
if( $menuType == 'footer' &amp;&amp;  $subPageOf == '0' ){
$footerMenu .= &quot;<a href='$pageUrl'&gt;$menuPageName</a&gt;&quot;;
}
$categoryMenu = '<ul&gt;'.$categoryMenu.'</ul&gt;';
echo $headerMenu
echo $categoryMenu
echo $footerMenu
This will output the first level of course... but how do I nest the sub-pages into the <li&gt; of its parent for the category menus.
I could put another whole foreach loop in the <li&gt; element and run it again filtering for the parent id. But this means I would have to hard code a depth to search for, and it seems like a lot of extra code. I am hoping for a more elegant solution.
Here is an example Json Menu
[{&quot;menu_pages_id&quot;:&quot;77&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;1&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Home&quot;,&quot;pageUrl&quot;:&quot;home&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;78&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;14&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;About Us&quot;,&quot;pageUrl&quot;:&quot;aboutus&quot;,&quot;pageType&quot;:&quot;content&quot;,&quot;isProtected&quot;:&quot;0&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;80&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;6&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Register&quot;,&quot;pageUrl&quot;:&quot;register&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1004&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;81&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;7&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Your Account&quot;,&quot;pageUrl&quot;:&quot;account&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1003&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;82&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;9&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Logout&quot;,&quot;pageUrl&quot;:&quot;logout&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1003&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;83&quot;,&quot;menu_id&quot;:&quot;2&quot;,&quot;page_id&quot;:&quot;15&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Terms and Conditions&quot;,&quot;pageUrl&quot;:&quot;terms-and-conditions&quot;,&quot;pageType&quot;:&quot;content&quot;,&quot;isProtected&quot;:&quot;0&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Footer&quot;,&quot;menuType&quot;:&quot;footer&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;84&quot;,&quot;menu_id&quot;:&quot;2&quot;,&quot;page_id&quot;:&quot;44&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Wholesale&quot;,&quot;pageUrl&quot;:&quot;wholesale&quot;,&quot;pageType&quot;:&quot;content&quot;,&quot;isProtected&quot;:&quot;0&quot;,&quot;customerGroup&quot;:&quot;1005, 1007&quot;,&quot;menuName&quot;:&quot;Footer&quot;,&quot;menuType&quot;:&quot;footer&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;97&quot;,&quot;menu_id&quot;:&quot;24&quot;,&quot;page_id&quot;:&quot;1&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Home&quot;,&quot;pageUrl&quot;:&quot;home&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Test Menu&quot;,&quot;menuType&quot;:&quot;category&quot;,&quot;assignedGroup_id&quot;:&quot;3&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;98&quot;,&quot;menu_id&quot;:&quot;24&quot;,&quot;page_id&quot;:&quot;14&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;About Us&quot;,&quot;pageUrl&quot;:&quot;aboutus&quot;,&quot;pageType&quot;:&quot;content&quot;,&quot;isProtected&quot;:&quot;0&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Test Menu&quot;,&quot;menuType&quot;:&quot;category&quot;,&quot;assignedGroup_id&quot;:&quot;3&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;99&quot;,&quot;menu_id&quot;:&quot;24&quot;,&quot;page_id&quot;:&quot;5&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;97&quot;,&quot;pageName&quot;:&quot;Login&quot;,&quot;pageUrl&quot;:&quot;login&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1004&quot;,&quot;menuName&quot;:&quot;Test Menu&quot;,&quot;menuType&quot;:&quot;category&quot;,&quot;assignedGroup_id&quot;:&quot;3&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;100&quot;,&quot;menu_id&quot;:&quot;24&quot;,&quot;page_id&quot;:&quot;3&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;99&quot;,&quot;pageName&quot;:&quot;Support&quot;,&quot;pageUrl&quot;:&quot;support&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Test Menu&quot;,&quot;menuType&quot;:&quot;category&quot;,&quot;assignedGroup_id&quot;:&quot;3&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;101&quot;,&quot;menu_id&quot;:&quot;24&quot;,&quot;page_id&quot;:&quot;15&quot;,&quot;listOrder&quot;:&quot;0&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Terms and Conditions&quot;,&quot;pageUrl&quot;:&quot;terms-and-conditions&quot;,&quot;pageType&quot;:&quot;content&quot;,&quot;isProtected&quot;:&quot;0&quot;,&quot;customerGroup&quot;:&quot;1001&quot;,&quot;menuName&quot;:&quot;Test Menu&quot;,&quot;menuType&quot;:&quot;category&quot;,&quot;assignedGroup_id&quot;:&quot;3&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;79&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;5&quot;,&quot;listOrder&quot;:&quot;3&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Login&quot;,&quot;pageUrl&quot;:&quot;login&quot;,&quot;pageType&quot;:&quot;system&quot;,&quot;isProtected&quot;:&quot;1&quot;,&quot;customerGroup&quot;:&quot;1004&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;},{&quot;menu_pages_id&quot;:&quot;96&quot;,&quot;menu_id&quot;:&quot;1&quot;,&quot;page_id&quot;:&quot;44&quot;,&quot;listOrder&quot;:&quot;8&quot;,&quot;subPageOf&quot;:&quot;0&quot;,&quot;pageName&quot;:&quot;Wholesale&quot;,&quot;pageUrl&quot;:&quot;wholesale&quot;,&quot;pageType&quot;:&quot;content&quot;,&quot;isProtected&quot;:&quot;0&quot;,&quot;customerGroup&quot;:&quot;1005, 1007&quot;,&quot;menuName&quot;:&quot;Header Menu&quot;,&quot;menuType&quot;:&quot;header&quot;,&quot;assignedGroup_id&quot;:&quot;1&quot;,&quot;menuStatus&quot;:&quot;Active&quot;}]
Comment: @YordanGeorgiev Can you give a code example?

Comment: Could you perhaps show you JSON file output. That would help.

Comment: Split into buildDir and buildItem, if item has items -> buildDir, else buildItem, wich will enable the hierarchy via recursion

Comment: not in php sorry ... but the principle is the same https://github.com/YordanGeorgiev/qto/blob/c6d65e72fc357bb0aa32e028b15df479fd96cae9/src/perl/qto/templates/controls/list-grid/js/left-menu.js.html.ep#L47

Here is the accepted answer: Recursion should be used i think. Some code based on rough example below shall work:
$menus = &quot;your flat menu array&quot;;
function getNestedMenu($menus,$subPageOf){
$nestedMenus = array();
foreach($menus as $menu){
if($menu['subPageOf'] = $subPageOf){
$nestedMenu = $menu;
$nestedMenu['subPages'] = getNestedMenu($menus,$menu['page_id']);
$nestedMenus[] = $nestedMenu;
}
return $nestedMenus;
$myNestedMenus = getNestedMenu($menu,&quot;0&quot;);
Using above code you can also adapt it to make nested ul li based menu html code
Comment for this answer: For 15 menu items, it is running the loop 432 times. Which seems like too many.  If there was a way to limit it to only run the function again if there was a child... but there is no way to know unless I run the loop. Any ideas?
Comment for this answer: As long as its normal, I thought I was doing something wrong. It was also very slow... but it turned out that's because I had an error in the code and PHP was outputting 432 entries in the error log with each run. When I fixed it, it was much faster!
Comment for this answer: I guess to convert a flat array to nested array based on parent, this is one of most straight forward ways. Let us know if you come across any other efficient one.

Here is another answer: Taking from what Kewlashu said I was able to make it work using the following.
$jsonMenu = json_decode($menus);
function makeMenu($jsonMenu,$parentId) {
$menu .=  &quot;<ul&gt;&quot;;
foreach ($jsonMenu as $pageInfo ) {
$menuCustomerGroup = $pageInfo-&gt;customerGroup;
$pageUrl = $pageInfo-&gt;pageUrl;
$menuPageName = $pageInfo-&gt;pageName;
$menuType = $pageInfo-&gt;menuType;
$subPageOf = $pageInfo-&gt;subPageOf;
$menu_pages_id = $pageInfo-&gt;menu_pages_id;
if($menuType == 'category' &amp;&amp;  $subPageOf == $parentId){
$menu .= &quot;<li&gt;<a href='$pageUrl'&gt;$menuPageName</a&gt;&quot;.makeMenu($jsonMenu,$menu_pages_id).&quot;</li&gt;&quot;;
}
$menu .= &quot;</ul&gt;&quot;;
return $menu;
$menu = makeMenu($jsonMenu,0);
$menu = str_replace(&quot;<ul&gt;</ul&gt;&quot;,'', $menu);
echo $menu;
This solution left empty <ul&gt;</ul&gt; elements at the bottom of each submenu so I used string replace to remove them before output.
Comment for this answer: This answer is not quite right, because it does a LOT of loops, more than are needed.
Comment for this answer: Well as long as it's ok to run that many times! I thought about caching the output, however, the menus might change depending on someone's status... logged in or different permission. I thought about just sending everything, and then using the front end to hide what should not be visible. However I rather not have the potential for the end user to be able to see something they are not supposed to.
Comment for this answer: Another way to look at it is - this use case is applicable to most of the cases where we need to make dynamic menus and looping a few thousand times (worst case) doesn't reflect badly on page load times. We can also implement a cache mechanism to only create nested structure only when the flat one changes.

{'source': 'StackExchangeDataset', 'subset': 'stackoverflow.com', 'metadata': {'AnswerCount': 2, 'CommentCount': 4, 'ContentLicense': 'CC BY-SA 4.0', 'CreationDate': '2020:00:07/11/20', 'FavoriteCount': None, 'PostTypeId': 1, 'Tags': '<php><json><menu>', 'Title': 'How to take a flat JSON file and build a hierarchical menu using PHP', 'ViewCount': 303, 'id': 62855037, 'user_AboutMe': None, 'user_AccountId': 8828185.0, 'user_DisplayName': 'David Edwards', 'user_Reputation': 82.0, 'user_Views': 14.0, 'user_id': 6595700.0}}