1
votes

My main developer is away so I'm stretching my capabilities as I'm not a programmer, competent to a level but not great, so bear with me.

We have a 2-level menu builder. It needs a 3rd level. Framework is CI.

DB table is 'pages_nav' Fields = navid (unique) | pageid | parentid | entry_type | target | order

Relates to table called 'pages' - I join pages_nav and pages to get URL and title

Entry_type is 1, 2 or 3 (top level, sub item or menu divider (horizonal rule/break between menu items)

DB lookup is:

$menu = $this->db
      ->select('P.id, P.title, P.url, N.entry_type, N.target, N.order, N.parentid, N.navid')
      ->join('pages P', 'P.id = N.pageid', 'left')
      ->order_by('N.parentid, N.order')
      ->get('pages_nav N')
      ->result_array();

Then I'm getting an array from this (all in the model), looping and passing back if parent or child like this:

if(!$menu) return [];
    $menu_out = [];
    foreach($menu as $item) {
      if($item['parentid'] == 0) {
        $menu_out[$item['id']] = ['details'=>$item, 'children'=>[] ];
      } else {
        $menu_out[$item['parentid']]['children'][] = $item;
      }
    }
    return $menu_out;

In the controller I'm just calling the model and passing $data['menu_items'] to the view as $data.

In the view i've got a foreach inside a foreach to print out the parents and any children, like this:

<?php if(!empty($menu_items)): ?>
    <?php foreach($menu_items as $item): ?>
        [ PRINT OUT ROWS: title, URL, Order etc]
        <?php if(count($item['children'])): ?>
            <?php foreach($item['children'] as $child): ?>
                [ PRINT OUT ROWS of child items: title, URL, Order etc]
            <?php endforeach; ?>
        <?php endif; ?>
    <?php endforeach; ?>
<?php else: ?>
    [ no entries found text ]
<?php endif; ?>

I don't want to change the DB with exception of adding a column if necessary. I don't want to totally re-build the code and I don't need recursive of any more levels than 3 (100% definite on that) so what I need is a stress-free, simple edit to what I have to allow a 3rd level but whatever I try I'm failing miserably so I'm looking for some direction.

What I've been looking at is having entry_type = 4 for 3rd level as an identifier but I'm struggling...

EDIT -- Now adding my View in to help:

<?php if(!empty($menu_items)): ?>
    <?php foreach($menu_items as $item): ?>
            <?php
            if($item['details']['target']==0)
                $link_target = "Current";
            else
                $link_target = "New";
            ?>
            <tr class="parent">
                <td><span class="anchor" data-id="<?php echo $item['details']['id']; ?>"><i class="fa fa-chevron-down <?php echo count($item['children']) ? '' : 'off'; ?>"></i> <?php echo $item['details']['title']; ?></span></td>
                <td><?php echo $item['details']['url']; ?></td>
                <td><?php echo $item['details']['order']; ?></td>
                <td><?php echo $link_target; ?> Tab</td>
            </tr>
            <?php if(count($item['children'])): ?>
                <?php foreach($item['children'] as $child): ?>
                    <?php
                    if($child['target']==0)
                        $link_target = "Current";
                    else
                        $link_target = "New";
                    ?>
                    <tr class="child">
                        <?php if($child['entry_type'] == 3): ?>
                            <td colspan>-- DIVIDER --</td>                                  
                        <?php else: ?>
                            <td><?php echo $child['title']; ?></td>
                        <?php endif; ?>
                        <td><?php echo $child['url']; ?></td>
                        <td><?php echo $child['order']; ?></td>
                        <td><?php echo $link_target; ?> Tab</td>
                    </tr>
                <?php endforeach; ?>
            <?php endif; ?>
    <?php endforeach; ?>
<?php else: ?>
    <tr>
        <td colspan="6">No navigation entries</td>
    </tr>
<?php endif; ?>

Here's the entries in the db. The last one is the 3rd level item, relating to two rows above (2nd level), which relates to rows above that (parent/top level).

enter image description here

So there's 3 top level items, the last of which has 3 children (1 is a divider line), and one of those children has a sub-child (or child itself).

1
The change, I would imagine, would start with your second code example ( the menu loop). As it stands, it's only creating 2 levels. Level one is based on finding where all parent id's = 0. This creates your "Main Menu". Level 2 is then the children of that item. Now, quick question, maybe I misunderstood what you typed below all that, but what exactly is the 3rd level you'r trying to add? Is it another DB value, or is it from something else within the php in relation to the children or parent? FYI, I <3 CI, we can get this worked out, just bear with me. - SpYk3HH
OK, by example it would be a menu in the front end like this: About US > The Company > Our History Currently parentid relates to pageid. And entry_type is just to help in the front end know what we're displaying. Although I mention front end, all this is just back end (the menu builder) at the moment, as it essentially shows the same. - tobzzz
Part of the problem is only half understanding what the model if else does. I feel like in the else I should just be saying if entry_type=4 then somehow pass back children AND sub-children else just children, and catch that in the view but it's melting my non-programmer brain. - tobzzz
i'm going to start an answer and build on it. As you're not a full time programmer, i'm going to start by trying to break this down so I can make sure we're on the same page ... make sure to check out the comments in each code section, as that's were I'll try to explain what I think i'm seeing - SpYk3HH
Thanks - I should probably note I haven't determined a way to store this yet so I'm open to suggestions if you have an answer based on that. As it stands I thought it would be parentid to pageid same as second level but identified by entry_type 2 (child) or 4 (sub-child). - tobzzz

1 Answers

0
votes

I imagine you're looking at something like the following:

Model

class Db_lookup extends CI_Model {
    function __construct() { parent::__construct(); }
    function callDBMenuItems() {
        //  this is simply your call to the database
        //  it retrieves *rows* (array entries) of items that will be like:
        //      index (0 based) [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ]
        $menu = $this->db
            ->select('P.id, P.title, P.url, N.entry_type, N.target, N.order, N.parentid, N.navid')
            ->join('pages P', 'P.id = N.pageid', 'left')
            ->order_by('N.parentid, N.order')
            ->get('pages_nav N')
            ->result_array();
    }
    function getMenuItems() {
        $menu = $this->callDBMenuItems();
        if(!$menu) return [];
        //  menu items were found, create an array to put results in
        $menu_out = [];
        foreach($menu as $item) {   //  loops through array
            //  check if adding first entry
            //  by the looks, I assume all top level (1st level)
            //  items have a parent id of '0'
            //  i'm guessing this is to signify they dont have a parent
            if($item['parentid'] == 0) {
                //  add entry to menu_out array, with 2 values
                //  'details' will contain the entire item
                //  'children' looks like it is preped for the else statement
                $menu_out[$item['id']] = ['details'=>$item, 'children'=>[] ];
            } 
            else {
                //  here, it appears we're adding sublevel (level 2) values
                //  level 2 items obviously have a parent id that is not 0
                //  and relates to the item id of an item that 
                //  had a parent id of 0 (aka, this item's parent)
                //  again, but to different calue, this everty's 
                //  value of 'children'.
                //  will gain an array entry of the entire item
                //  this will create and array of children all containing entire items
                $menu_out[$item['parentid']]['children'][] = $item;
            }
        }
        //  our final results, if items found, would in turn look something like:
        /*
            array $menu_out => [
                'id0' => [
                    'details' => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ],
                    'children' => [
                        [0] => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ],
                        [1] => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ]
                    ]
                ],
                'id1' => [
                    'details' => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ],
                    'children' => [
                        [0] => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ],
                        [1] => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ]
                    ]
                ]
            ]
        */
        return $menu_out;
    }
}

Controller

class Page extends CI_Controller {
    function index() {
        //  simmply loads the previously created model,
        // however, you might not see this if your programmer 
        //  was smart enough to add it to the auto-load feature of CI
        $this->load->model('Db_lookup');
        
        /*  this is the array used to pass php items to the view
            each item can be called in the view by using it's
            associative name
            for example, if the array has an entry like [ 'bob' => 'bill' ],
            then in the view, you can get 'bob' simply by calling:
            <?php echo $bob; ?>
            just think of each entry as a declaration of a PHP variable, 
            without the $ sign. You then simply add the $ sign to that item's name
            in the view in order to get that item.  */
        $data = [];
        //  here we add an item to our array that will be passed to the view
        //  note how the entry is 'named'. This is the value we'll 
        //  use to call it in the view
        $data['menu_items'] = $this->Db_lookup->getMenuItems();
        //  load the 'view' (aka, the page you see in the browser)
        //  and pass our data array to it
        $this->load->view('pageName', $data);
    }
}

View

<!--    as noted before, you can call items from the data array passed through
    simply by adding $ sign to the begining of the item's index name.   -->
<?php if(!empty($menu_items)): ?>
    <ul>
    <!--    since we know `menu_items` is an array, as previously defined in our model,
            then we know we can loop for it based on the creation of it in our model    -->
    <?php foreach($menu_items as $item): ?>
        <!--    [ PRINT OUT ROWS: title, URL, Order etc]    -->
        <?php if(count($item['children'])): ?>
            <!--    I don't see where you've created actual HTML yet, 
                but I assume you're looking for something like: -->
            <li id="order_<?php echo $item['order']; ?>">
                <a href="<?php echo $item['url']; ?>"><?php echo $item['title']; ?></a>
                <ul>
                <?php foreach($item['children'] as $child): ?>
                    <!--    [ PRINT OUT ROWS of child items: title, URL, Order etc] -->
                    <!--    Again, I don't see any HTML you've created, 
                        but this may be something like: -->
                    <li id="order_<?php echo $child['order']; ?>">
                        <a href="<?php echo $child['url']; ?>"><?php echo $child['title']; ?></a>
                    </li>
                <?php endforeach; ?>
                </ul>
            </li>
        <?php endif; ?>
    <?php endforeach; ?>
    </ul>
<?php else: ?>
    <!--    [ no entries found text ]   -->
    <p>Sorry, No Menu Found</p>
<?php endif; ?>

The Fix

In trying to think of the most "headache free" way to handle this, I decided you'd need to make a few small changes throughout the process.

First of all, you'll need to make some small changes to expected array. Not sure if that model is used in other places (i imagine it is), so we won't change the model. Instead, we'll start at changing the data in the controller. This way, you can pass a new array, with 3 levels of data, to the view.

Controller

function index() {
    $this->load->model('Db_lookup');
    
    //  first let's get the uncut menu_items
    $menu_items = $this->Db_lookup->getMenuItems();
    //  now let's break them down and build a new
    //  array to pass to the view.
    //  this could be done even more easy by changing
    //  the model, however, it's unknown (to me at least)
    //  if that model is used in other places.
    //  it likely is, so let's just modify here instead.
    //  first i'll setup 3 simple arrays. This isn't
    //  the only way to do this, and could be made more
    //  condensed, but for sake of less headache,
    //  i'm breaking it into a visually easy to see array
    //  of the items we want.
    $parent_items = [];
    //  let's make this a little easier, 
    //  if you look back to the array returned from the model,
    //  you'll notice each item should already have and index ID,
    //  so I'll use that in the foreach loop
    foreach ($menu_items as $id => $item) {
        //  at this point, the following 'if' should always be true
        if(isset($item['details']) && $item['details']['parentid'] == 0) {
            //  setup array with easily referable id
            //  mimicking original setup
            $parent_items[$id] = [
                'details' => $item['details'],
                'children' => []    //  empty children, since we want to change how children is handled
                //  by adding a level 3 component
            ];
            //  first, let's get this parent's child items,
            //  but we only want the one's that don't have
            //  `entry_type=4`
            if (!empty($item['children'])) {
                //  let's make this easier to follow
                $uncutChildren = $item['children'];
                //  and make an easy child array to work with,
                //  that will later add back to parent
                $newChildren = [];
                
                //  first let's add the level 2 (children) only
                //  using an $i loop may not have been best approach,
                //  so instead, I'll make a retlational ID,
                //  based on info within the child item
                foreach ($uncutChildren as $child) {
                    //  $child should look like:
                    //  [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ]
                    
                    //  if i'm reading the output of the view you posted correctly,
                    //  entry_type 3 is always between your expected children,
                    //  so I assume, every child between 3's should be a level 2,
                    //  OR a level 3 of the previously pulled 2,
                    $childID = count($newChildren);
                    //   with that in mind, try this
                    switch ($child['entry_type']) {
                        case 2:
                        case 3: // adds new entry
                            $newChildren[$childID] = $child;
                            break;
                        case 4:
                            if (!empty($newChildren[$childID-1])) $newChildren[$childID-1]['level3'] = $child;
                            break;
                    }
                }
                
                //  now add in our new children
                $parent_items[$id]['children'] = $newChildren;
            }
        }
    }
    // this should help make a new array that looks like:
    /*
        array $menu_out => [
            'id0' => [
                'details' => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ],
                'children' => [
                    [0] => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid',
                        'level3' => [ 'id', 'title', 'url', 'entry_type', 'target', 'order', 'parentid', 'navid' ]
                    ]
                ]
            ]
        ]
    */
    $data = [ 'menu_items' => $parent_items ];
    $this->load->view('pageName', $data);
}

Now you should have a new level to work with in your view!

<?php if(!empty($menu_items)): ?>
    <?php foreach($menu_items as $item): ?>
            <?php $link_target = $item['details']['target']==0 ? "Current" : "New"; ?>
            <tr class="parent">
                <td>
                    <span class="anchor" data-id="<?php echo $item['details']['id']; ?>">
                        <i class="fa fa-chevron-down <?php echo count($item['children']) ? '' : 'off'; ?>"></i> <?php echo $item['details']['title']; ?>
                    </span>
                </td>
                <td><?php echo $item['details']['url']; ?></td>
                <td><?php echo $item['details']['order']; ?></td>
                <td><?php echo $link_target; ?> Tab</td>
            </tr>
            <?php if(count($item['children'])): ?>
                <?php foreach($item['children'] as $child): ?>
                    <?php $link_target = $child['target']==0 ? "Current" : "New"; ?>
            <tr class="child">
                <?php if($child['entry_type'] == 3): ?>
                    <td colspan>-- DIVIDER --</td>                                
                <?php else: ?>
                    <td><?php echo $child['title']; ?></td>
                <?php endif; ?>
                <td><?php echo $child['url']; ?></td>
                <td><?php echo $child['order']; ?></td>
                <td><?php echo $link_target; ?> Tab</td>
            </tr>
            <!--    ¡¡¡NEW LEVEL!!! -->
            <!--    Not really sure where you want it, so I just created a new row,
                    with a colored background so you can "see" the change   -->
                    <? php if (!empty($child['level3'])): ?>
                        <?php foreach($child['level3'] as $lvl3): ?>
                        <?php $link_target = $lvl3['target']==0 ? "Current" : "New"; ?>
            <tr style="background: yellow;">
                <td><?php echo $lvl3['title']; ?></td>
                <td><?php echo $lvl3['url']; ?></td>
                <td><?php echo $lvl3['order']; ?></td>
                <td><?php echo $link_target; ?> Tab</td>
            </tr>
                        <?php endforeach; ?>
                    <?php endif; ?>
                <?php endforeach; ?>
            <?php endif; ?>
    <?php endforeach; ?>
<?php else: ?>
    <tr>
        <td colspan="6">No navigation entries</td>
    </tr>
<?php endif; ?>

I'm working back and forth between a bunch of my own real work and this, so just comment if something didn't quite turn out right, and i'll try to fix it! Good luck!