Chris Clower
Chris Clower

Reputation: 5104

PHP MySQL Menu Sorting

Ok, so here's my table structure:

+--------------------------+ +----------------+ +-------------------------------+
|          pages           | |      menus     | |           menu_pages          |
+--------------------------+ +----+-----------+ +-------------------------------+
|   id  |  title  | slug   | | id |  name     | | menu_id | page_id | parent_id |
+-------+---------+--------+ +----+-----------+ +---------+---------+-----------+
|   1   |  Home   | index  | | 1  |  default  | |    1    |    1    |     0     |
+-------+---------+--------+ +----+-----------+ +---------+---------+-----------+
|   2   |  About  | about  | | 2  |  footer   | |    1    |    2    |     0     |
+-------+---------+--------+ +----+-----------+ +---------+---------+-----------+
|   3   | Test 1  | test-1 |                    |    1    |    3    |     2     |
+-------+---------+--------+                    +---------+---------+-----------+
|   4   | Test 2  | test-2 |                    |    1    |    4    |     2     |
+-------+---------+--------+                    +---------+---------+-----------+
|   5   | Test 3  | test-3 |                    |    1    |    5    |     4     |
+-------+---------+--------+                    +---------+---------+-----------+

So basically, we have pages, menus, and a menu_pages linking table which specifies the menu, the page, and the parent of each menu item.

Here's my query:

$query = "SELECT pages.id, pages.title, pages.slug, menu_pages.parent_id
          FROM menus, pages, menu_pages WHERE menus.name = '$menu'
          AND menus.id = menu_pages.menu_id
          AND pages.id = menu_pages.page_id";

$results = $db->Query($query);

Here's the question: How do I get the menu items properly nested under their respective parents in an array? I've tried quite a few things already, but none of them worked beyond simply 2 levels, so I won't clutter up the question with it. Obviously I need some kind of recursion in PHP, or to modify my query maybe in a way that I can get the SQL to return them properly?

It should look something like this in the output:

[0] => array(
  'id'        => 1,
  'title'     => 'Home',
  'slug'      => '/',
  'parent_id' => '0'
)
[1] => array(
  'id'        => 2,
  'title'     => 'About',
  'slug'      => 'about',
  'parent_id' => 0,
  'sub_menu'  => array(
    [0] => array( 
      'id'        => 3,
      'title'     => 'Test 1',
      'slug'      => 'test-1',
      'parent_id' => 2
    )
    [1] => array(
      'id'        => 4,
      'title'     => 'Test 2',
      'slug'      => 'test-2',
      'parent_id' => '2',
      'sub_menu'  => array(
        [0]           => array(
          'id'        => 5,
          'title'     => 'Test 3',
          'slug'      => 'test-3',
          'parent_id' => 4
        )
      )
    )
  )
)

Thanks for the help!

Upvotes: 2

Views: 330

Answers (1)

Rob Baillie
Rob Baillie

Reputation: 3460

This isn't quite as simple as it first sounds - if you want to get into how to do it with SQL, you are looking for a recursive sql statement.

Unfortunately mysql doesn't support this directly, and you would need to write a body of code to get it working. There is an example of how to do it here. Not simple.

In case you're interested in how to pick this apart, you would implement it in Oracle like this:

SELECT p.id, p.title, p.slug, mp.parent_id, level
FROM   menu_pages   mp
JOIN   pages        p ON ( p.id = mp.page_id )
JOIN   menus        m ON ( m.id = mp.menu_id )
CONNECT BY PRIOR mp.page_id = mp.parent_id
START WITH ( m.name = 'default' AND mp.parent_id = 0 )

You are basically saying:

  • START WITH a query for the top level of the menu
  • CONNECT that back to the result set by joining the parent to the child

You end up with a result set like this:

id  title   slug    parent  level
------------------------------------
 1  Home    index   0   1
 2  About   about   0   1
 3  Test 1  test-1  2   2
 4  Test 2  test-2  2   2
 5  Test 3  test-3  4   3

All this actually gives you in addition to what you already have is:

  • The "level" of each point in the menu.
  • If a sub menu appeared multiple times in your structure it would be repeated correctly.
  • Sections of the menu that are not connected properly will not be returned.

So, for small, simple and consistent menus it's probably over-kill anyway.

Plus, even in this case you would need to process this in PHP to get the structure you're looking for.

So, using that as inspiration you can see how you could implement it in mysql by just doing the post processing.

You start off with your original query:

SELECT pages.id
     , pages.title
     , pages.slug
     , menu_pages.parent_id
FROM menus
   , pages
   , menu_pages
WHERE menus.name = 'default'
AND menus.id = menu_pages.menu_id
AND pages.id = menu_pages.page_id

You can then loop over this result and build the array structure yourself manually.

In order to avoid the problem of recursion, we're instead going to take advantage of the fact that we can have two variables pointing at the same data structure - we're going to use references so that changing the value of the variable in one reference will change the value of the variable in the other.

I.E. The difficulty you get is finding the right point in the hierarchy to add each child. With references you don't have to.

  • Create an empty menu array
  • Loop over the results from your SQL statement
  • Create a copy of each menu item and put it into a simply indexed store (by the id of the item)
  • If you have the root menu item, add it to your menu array (as a reference)
  • If you don't have the root menu item, find the parent in your simple store and add your new item to it.

At the end you should have the nice nested structure you're looking for.

Like this:

<?php

// As if it came back from mysql...
// Assumed that it's ordered so that every possible parent appears before all its childern

$aResults = array( array( 'id' => 1, 'title' => 'Home',   'slug' => 'index',  'parent_id' => 0 )
                 , array( 'id' => 2, 'title' => 'About',  'slug' => 'about',  'parent_id' => 0 )
                 , array( 'id' => 3, 'title' => 'Test 1', 'slug' => 'test-1', 'parent_id' => 2 )
                 , array( 'id' => 4, 'title' => 'Test 2', 'slug' => 'test-2', 'parent_id' => 2 )
                 , array( 'id' => 5, 'title' => 'Test 3', 'slug' => 'test-3', 'parent_id' => 4 ) );

// the menu you're creating
$aMenu          = array();

// the simple store of the menu items you're going to use to find the parents
$aBaseMenuIndex = array();

foreach( $aResults as $aMenuItem ) {

    $aMenuItem['sub_menu'] = array();

    // add your menu item to the simple store
    $aBaseMenuIndex[ $aMenuItem['id'] ] = $aMenuItem;

    if ( $aMenuItem['parent_id'] == 0 ) {

        // if it's a base menu item, add it to the menu
        $aMenu[] =& $aBaseMenuIndex[ $aMenuItem['id'] ];

    } else {

        // if it's not a base item, add it to the sub menu, using the simply indexed store to find it
        // adding it here will also add it to $aMenu, as $aMenu contains a reference to this
        $aBaseMenuIndex[ $aMenuItem['parent_id'] ]['sub_menu'][] =& $aBaseMenuIndex[ $aMenuItem['id'] ];
    }
}

var_dump( $aMenu );

Upvotes: 1

Related Questions