7

SOLVED! Final answer is located at the bottom of this question

I'm trying to make a menu builder using CodeIgniter, and for some reason, I can't seem to wrap my head around the concept, even though it seems simple enough (I'm new to PHP and CI). This should have less to do with CodeIgniter itself really, as I'm really only using it for the queries and MVC pattern.

I have two tables:

  • menus:

    • id
    • name
  • menu_pages:

    • id
    • page_id (relational to pages.id)
    • menu_id (relational to menus.id)
    • item_name (how it appears in the menu)
    • item_order (for sorting)
    • item_parent (for nesting items in sub menus)

EDIT: This is the structure I'm looking to achieve:

array(  
    [0] => array(
        [menu_id]    => 1,
        [menu_name]  => 'Menu 1',
        [menu_pages] => array(
            [0] => array(
                [id]         => 1,
                [page_id]    => 1,
                [menu_id]    => 1,
                [item_name]  => 'Home',
                [item_order] => 0,
                [item_level] => 0,
                [parent_id]  => ''
            ),
            [1] => array(
                [id]         => 2,
                [page_id]    => 2,
                [menu_id]    => 1,
                [item_name]  => 'About',
                [item_order] => 0,
                [item_level] => 0,
                [parent_id]  => ''
            )
        )
    ),
    [1] => array(
        [menu_id]    => 2,
        [menu_name]  => 'Menu 2',
        [menu_pages] => array(
            [0] => array(
                [id]         => 3,
                [page_id]    => 3,
                [menu_id]    => 2,
                [item_name]  => 'Services',
                [item_order] => 0,
                [item_level] => 0,
                [parent_id]  => ''
            ),
            [1] => array(
                [id]         => 4,
                [page_id]    => 4,
                [menu_id]    => 2,
                [item_name]  => 'Contact',
                [item_order] => 0,
                [item_level] => 0,
                [parent_id]  => ''
            )
        )
    )
)

Here's what I've got so far (UPDATED):

function GetMenus() {
    $menus    = $this->db->get('menus');
    $menucols = $this->db->list_fields('menus');

    $pages    = $this->db->get('menu_pages');
    $pagecols = $this->db->list_fields('menu_pages');

    $arr      = array();
    $i        = 0;

    foreach($menus->result() as $menu) {
        foreach($pages->result() as $page) {
            foreach($pagecols as $col) {
                $arr[$i][$col] = $page->$col;
            }

            foreach($menucols as $cols) {
                $this->menus[$i][$cols] = $menu->$cols;

                if($arr[$i]['menu_id'] === $menu->id) {
                    $this->menus[$i]['menu_pages'] = $arr[$i];
                }
            }
        }

        $i++;
    }

    return $this->menus;
}

The above actually outputs this:

Array(
    [0] => Array(
        [id] => 1,
        [name] => default,
        [menu_pages] => Array(
            Array( /* missing #1, showing #2/2 */
                [id] => 2
                [page_id] => 1
                [menu_id] => 1
                [item_name] => About
                [item_order] => 0
                [item_level] => 0
                [parent_id] => 
            )
        )
    ),
    [1] => Array(
        [id] => 2,
        [name] => menu2,
        [menu_pages] => Array(
            Array( /* missing #3, showing #4/4 */
                [id] => 4
                [page_id] => 3
                [menu_id] => 2
                [item_name] => Contact
                [item_order] => 0
                [item_level] => 0
                [parent_id] => 
            )
        )
    )
)

As you can see, this is pretty close to what I need, but there are missing items because it seems as though they are getting overwritten in the array (it is only showing the last menu item for each menu - seems maybe they have the same keys).

Thanks for any and all help and suggestions!

EDIT: Here is the final solution, loosely based on Mischa's answer:

Model:

function GetMenus() {
    /* Yes, I know these can be chained, I unchained them to avoid
       horizontal scrolling on SO */
    $this->db->select('menus.name, menu_pages.*, pages.slug');
    $this->db->join('menu_pages', 'menu_pages.menu_id = menus.id');
    $this->db->join('pages', 'pages.id = menu_pages.page_id');
    $this->db->order_by('item_order', 'ASC');
    $menus  = $this->db->get('menus');
    $result = array();

    foreach($menus->result() as $menu) {
        $result[$menu->name][$menu->id] = array(
            'page_id'    => $menu->page_id,
            'menu_id'    => $menu->menu_id,
            'item_name'  => $menu->item_name,
            'item_slug'  => $menu->slug,
            'item_order' => $menu->item_order,
            'item_level' => $menu->item_level,
            'parent_id'  => $menu->parent_id
        );
    }

    return $result;
}

Controller:

$this->menu = $this->page->GetMenus();

View:

<ul class="nav">
    <?php foreach($this->menu['default'] as $item) { ?>
        <li>
            <a href="<?php echo $item['item_slug']; ?>">
                <?php echo $item['item_name']; ?>
            </a>
        </li>
    <?php } ?>
</ul>
Chris Clower
  • 5,036
  • 2
  • 18
  • 29

4 Answers4

3

Try this one:

function GetMenus() {
    $menus = $this->db->get('menus');
    $pages = $this->db->get('menu_pages');
    $cols  = $this->db->list_fields('menu_pages');

    foreach($menus->result() as $menu) {
        $this->menus[$menu->id] = $menu;
        foreach($pages->result() as $page) {
            if($page->menu_id === $menu->id) { 
                foreach($cols as $col) {
                    $this->menus[$menu->id][$col][] = $page->$col;
                }
            }
        }
    }

    print_r($this->menus); // for debugging to see result

    return $this->menus;
}
Code Prank
  • 4,209
  • 5
  • 31
  • 47
  • well Shayan That's great using list_fields which people usually avoid to use – Muhammad Raheel Aug 03 '12 at 07:44
  • @raheelshan why do people avoid using `list_fields`? It's very useful if you want to store all your fields and their data in an array without typing out every single one. You could have 100 columns in a table that you could store in an array in 1 line of code instead of 100 lines. Is there a security risk to using list_fields or something? – Chris Clower Aug 03 '12 at 20:05
  • I'll give this a try. I thought about incrementing a var before, but the potential issue I see with this is that the menus are going to be numbered in some unknown order, and would have to be accessed by knowing the number that was assigned to it in the loop. What about something like: `$this->menus[$menu->id][$col] = $page->$col;`? Then I assume it could be accessed by its actual id, but I'm not sure if it'd work. – Chris Clower Aug 03 '12 at 20:09
  • This is pretty close. Very close. I edited my initial post to give more of an idea of the array structure I need, but what is returned by this is the closest thing I've tried. If you can help me get this formatted as in my edited sample array above, I'll definitely accept it. Thanks! – Chris Clower Aug 04 '12 at 07:05
  • 1
    @ChrisClower i have updated my code please check and let me know whether it works or not – Code Prank Aug 04 '12 at 09:23
  • @ShayanHusaini take a look at my updated code and let me know what you think. It's still not right, but I added the output below it. The structure is right but the items under '[menu_pages]' in the code are getting the same key so only the last one is getting stored. – Chris Clower Aug 04 '12 at 16:36
  • 1
    @ChrisClower please put that empty index in front of $col i have updated my code – Code Prank Aug 05 '12 at 06:53
  • @ShayanHusaini it's somewhat close, but we do need the menu names as well. See my revised code and desired array structure. Thanks for your help so far! – Chris Clower Aug 05 '12 at 06:58
  • 1
    ok now i have added the menu data in array as well. Try it and let me know if it works. – Code Prank Aug 05 '12 at 07:21
  • @ShayanHusaini The most recently updated code gives this: `Fatal error: Cannot use object of type stdClass as array`, referring to this line: `$this->menus[$menu->id][$col][] = $page->$col;`. – Chris Clower Aug 05 '12 at 07:39
  • 1
    ok convert this line into $this->menus[$menu->id]['menu_pages'][] = $page->$col – Code Prank Aug 05 '12 at 07:45
  • @ShayanHusaini Got it solved - I upvoted your answer and comments so you get some decent rep - sorry you didn't get the bounty, but thanks again for your hard work! :) – Chris Clower Aug 05 '12 at 08:17
  • I feel sorry for steeling the bounty, so I also upvoted your answer ;-) – Mischa Aug 05 '12 at 08:20
3

You have two queries, but I think I would opt for a single query with a join. This makes the code shorter and less complex. I haven't tested the code below, but something like this should work:

function GetMenus()
{
  $this->db->select('menus.name, menu_pages.*');
  $this->db->join('menu_pages', 'menu_pages.menu_id = menus.id');
  $this->db->order_by('menus.id');
  $q = $this->db->get('menus');

  $result = array();
  $current_menu_id = NULL;
  $i = -1;

  foreach($q->result() as $row)
  {
    if($current_menu_id !== $row->menu_id)
    {
      $i++;
      $result[] = array('menu_id' => $row->menu_id,
                        'menu_name' => $row->name,
                        'menu_pages' => array()
                  );
    }

    $result[$i]['menu_pages'][] = array('id' => $row->id,
                                        'page_id' => $row->page_id,
                                        'menu_id' => $row->menu_id,
                                        'item_name' => $row->item_name,
                                        'item_order' => $row->item_order,
                                        'item_level' => $row->item_level,
                                        'parent_id' => $row->parent_id
                                  );

    $current_menu_id = $row->menu_id;
  }

  return $result;
}
Mischa
  • 42,876
  • 8
  • 99
  • 111
  • Wow, I think you got it (without even testing the code?! I pasted it straight in without change!) The array I just printed is beautiful! Give me a few minutes to add a couple records and test, but I think this is the one! – Chris Clower Aug 05 '12 at 08:06
  • We have a winner! Thanks so much! :D – Chris Clower Aug 05 '12 at 08:15
  • It says I have to wait 19 hours to award the bounty, but I definitely will! – Chris Clower Aug 05 '12 at 08:15
  • Glad it works. This doesn't take into account the `parent_id` stuff. If you want to take nested menus into account the code will get more complicated, but for what you describe in your question this is the simplest solution, I think. – Mischa Aug 05 '12 at 08:18
  • Yeah, that's going to be the next step (the parents and children), which could get messy because it's going to add potentially many more dimensions and nested arrays. To me, this was one hella complicated array to try to build for what's here now, lol. – Chris Clower Aug 05 '12 at 08:28
0
 $sql = "
    select 
    id , 
    name ,
    group_concat(page_id) as page_ids,
    group_concat(item_name) as item_name,
    group_concat(item_order ) as item_order ,
from menues 
left join menu_pages on menu_pages.menu_id = menues .id";

$query = $this->db->query($sql);
return $query->result();

And done expolde the cells on php end

Muhammad Raheel
  • 19,823
  • 7
  • 67
  • 103
  • if you dont understand first print_r() the result to see what is coming – Muhammad Raheel Aug 03 '12 at 04:20
  • Thanks for your reply, but I don't think there's any need to do a join here since page_id is already relational to pages.id and menu_id is relational to menus.id as index. I'm already able to get the results of menu_id and and page_id, so it seems I should be able to just use php to build an array of all the menus and the pages that belong to them? I could be wrong - please explain further. – Chris Clower Aug 03 '12 at 04:24
  • Well if i were you i would use single table with self join recognizing parent id 0 for parents and > 0 for sub menues here are some tutorials http://www.phpeveryday.com/articles/CodeIgniter-Form-Creating-Menu-Library-P230.html and see this forum too http://codeigniter.com/forums/viewthread/98774/ – Muhammad Raheel Aug 03 '12 at 04:38
  • It started as one table, but I found the need to separate them for various reasons. For example, each row in the "menus" table is a new menu, while each row in the "menu_pages" table represents a page, which is associated with a menu id (`menu_pages.page_id => menus.id`). That's why I think I can't add actual pages to the menus table - menus and their associated pages seem to need to be in separate but relational tables. – Chris Clower Aug 03 '12 at 04:43
0

You could declare $this->menus as :

$this->menus = array()

and use array_push() function like so:

array_push($this->menus,$page->$col);

I would also recommend to do join operation in model rather than looping in your function because database would do it fast and your code would look elegant.

Daniel
  • 23,129
  • 12
  • 109
  • 154
Skandh
  • 426
  • 3
  • 18
  • I already have `$menus = array()` at the top of the class, sorry for not clarifying (although it was implied through use of `$this->menus` in code). No need for `array_push()` as far as I can tell, since `$menus['key'] = $val` should do the same thing. It needs to be multidimensional. Thanks for the reply! – Chris Clower Aug 03 '12 at 04:28