How to build your own Multilingual PHP MVC CMS from scratch - Part 4 - Frontend (part 2): Menu System

Sunday, December 28, 2014

Welcome to the fourth settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which, this time, we will lay our hands on the front-end menu system.

The last time, we have covered the complete building of the (front) page module, going from the database page table up to page.php, in which the required page content is displayed. In between, we have exposed and explained the page controller and its corresponding model, while also demonstrating, among other things, a way to create and instantiate a Singleton class. All of which having resulted in the making of our first complete and fully functional module that allows the user, upon manually loading the correct URL in the browser, to fetch and display on screen a requested page.

But although doing this works perfectly well, one cannot expect nowadays to have users always 'hard-type' the URL of the resource they seek in their browsers. That is one of the reasons why menus have generally been accepted as an extremely important feature of modern Web development. Since the aim of any CMS is by nature to make the programmer/administrator's life easier while trying to meet the most functionality that's the most frequently asked for in the Web creation industry, we're going to do just that: to implement a menu system in our CMS.

Note however, before we begin, that each forthcoming settlement will tend to reflect the CMS's own business logic gradually more, rather than get to explain the same PHP-OOP language features over and over again. This does make sense since we have already reviewed the most part of the core object model on which our project is based.

Our menu system will have its own back-end management module, which we will be covering in a future settlement only. But for now, we will set our focus onto its front-end side, starting from the database model and going all the way to visual display.

So let's jump into the database model first. Basically, from a back-end point of view, the administrator will have the possibility to create, edit and delete whole menus. What this means for us on the front-end side is that, at any time, there could be any number of menu tables in the database, named menus_1, menus_2 and menus_3, etc... . Each menu table containing any number of menu rows (entries).

Still on the front-end side, two types of graphical menu layouts will be available for now, both relying on Javascript/CSS via third-party scripts, one of which displaying a layout of vertical type (menu_tree.php) and the other one, of horizontal type (hmenu.php). Both menu layouts being multilevel, meaning they will support an unlimited number of levels.

The Javascript files for those libraries are named respectively dtree.php and hmenu.js and reside in public/js. As for CSS, they are named dtree.css and hmenu.css and can be found in themes/your_style/css. Theme icons for the dtree library will be set inside its Javascript file, but will be found in themes/your_style/servicing/dtree. Icons for the hmenu library will be found in themes/your_style/servicing/hmenu.

Now, In config.php, we will have to manually determine which menu table is to be assigned to either of those two graphical menu types, as shown bellow
define("MENU_TREE_MENU_TABLE","1");
define("HMENU_MENU_TABLE","2");
As an example, the value "1" simply corresponds to the table named menus_1 and "2" to menus_2.

There is definitely room for new graphical layout types or more of the existing ones, as long you declare the corresponding constants in config.php, at least to keep everything clean there. That said, in nowadays internet, Websites displaying more than two graphical menus at once on the same page are not that frequently encountered.

In any case, now is a good time to reveal the structure of our menu_# tables
--
    -- Table structure for table `menus_1`
    --
    
    CREATE TABLE IF NOT EXISTS `menus_1` (
    `id_menu_1` smallint(6) NOT NULL,
      `menu_1_rank` varchar(12) NOT NULL,
      `menu_1_type` varchar(60) NOT NULL,
      `menu_1_page` varchar(60) DEFAULT NULL,
      `menu_1_position` smallint(6) NOT NULL,
      `menu_1_display` enum('no','yes') DEFAULT NULL
    ) ENGINE=MyISAM AUTO_INCREMENT=43 DEFAULT CHARSET=latin1;
    
    --
    -- Dumping data for table `menus_1`
    --
    
    INSERT INTO `menus_1` (`id_menu_1`, `menu_1_rank`, `menu_1_type`, `menu_1_page`, `menu_1_position`, `menu_1_display`) VALUES
    (1, '1#0', 'page', 'hours', 1, 'yes'),
    (2, '2#0', 'page', 'products', 2, 'yes'),
    (3, '3#0', 'page', 'about_us', 3, 'yes'),
    (4, '4#0', 'page', 'directions', 4, 'yes');
    
    --
    -- Indexes for dumped tables
    --
    
    --
    -- Indexes for table `menus_1`
    --
    ALTER TABLE `menus_1`
     ADD PRIMARY KEY (`id_menu_1`);
    
    --
    -- AUTO_INCREMENT for dumped tables
    --
    
    --
    -- AUTO_INCREMENT for table `menus_1`
    --
    ALTER TABLE `menus_1`
    MODIFY `id_menu_1` smallint(6) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=5;
  • We can tell by the menus_#_display field that each menu row can either be chosen for display displayed or not.
  • You may already be recognizing the menu_#_type field, which corresponds the module type since our menu system will only be linking modules.
  • Row ranks (menu_#_rank) hold a special format [1 to 2 figures + # + 1 to 2 figures], and a row rank with 0 as right side value means it is a top-level row. No menu_rank entry can hold a 0 on its left side.
  • A parent-kid relation is obtained by having the right side of the kid's rank being equal to the left side of the parent's rank.
  • Modules other than the page or the elink module will have 0 as menu_#_page value since they don't need a page or elink. Elink will be the module that takes care of External links, used in case we need to link an external URL to a menu row.
That is all we need to know for now regarding the menu model at database level. So now we will be turning to MenuModel.php, and see how we will format our database menu table data to properly supply both our tree and horizontal menu layouts the feed formats they require.
<?php
    /* application/models/MenuModel.php */    

    class MenuModel{ 
        //Instantiating our class which appears to be a Singleton one again
    
        static $instance;    //declaring properties
        private $menu_items;
        private $module_name;
        private $items_translation_line;    
        private $items_translations;
    

        public static function getInstance(){ 
            //creating and returning a single static instance of the class for when invoked
            if(self::$instance ==  null)
                self::$instance = new self();
            return self::$instance;
        }
    
        public function __construct(){} //disallowing the creation of a new instance
        private function __clone(){} // disallowing cloning
    
        public function get_module_name(){ //returns the module name
            return $this->module_name = 'menu';
        }    
    
        public function getMenuItemsData($mtp, $direction = NULL){ //$mtp is the mumber of the menu we seek
            //this method fetches and prepares the database data needed to feed the menu layout  
            
            $util = new UtilsModel();
    
            // we first get all the exist menu tables
            $get_all_tables = $util->show_tables(DBNAME,'menus'); 
    
            if(in_array($mtp,$get_all_tables)){ // if the menu we seek exists 
    
                $mtp = "_".$mtp;
                $myitems = '';
    
                $menu_label_translations = self::getTreeMenuItemTranslations(CURRENT_LANG,'page'); // we fetch all page name translations for the current language (this will be useful when the module name is 'page')
                $menu_label_translations_elink = self::getTreeMenuItemTranslations(CURRENT_LANG,'elink'); // same for elinks but we do not need to worry about it for now
    
                //we select all the rows meant for display, order them by position, ascending 
                $my_menu_items = "SELECT id_menu".$mtp.", menu".$mtp."_rank, menu".$mtp."_type, menu".$mtp."_page 
                                     FROM menus".$mtp." 
                                     WHERE menu".$mtp."_display = 'yes' 
                                     ORDER BY menu".$mtp."_position ASC
                                   ";        
                
                MySQLModel::get_mysql_instance()->executeQuery($my_menu_items);
            
                if(MySQLModel::get_mysql_instance()->affectedRows() > 0){ //if we found at least one row

                    $ct_top_menus = 0;
    
                    while($myitems = MySQLModel::get_mysql_instance()->getRows($my_menu_items)){    
                        //while we pull and stack each found row in $myitems
    
                        if($myitems['menu'.$mtp.'_type'] == 'page'){//if row type equals page
                            
                            $mt = trim($myitems['menu'.$mtp.'_page']); //we fetch its formatted page name
                            $cv = 0;                
    
                            foreach($menu_label_translations as $mlt){
    
                                if(!in_array($mt,$mlt)){$cv++;}
                                else{
                                        //and fetch its non-formatted internationalized name to stack it in $menu_text, 
                                        //which corresponds to the menu label that will be displayed on screen
                                        if($mlt[1] == CURRENT_LANG){$menu_text = $mlt[2];
                                }
                            }
                        } 
    
                            if($cv == sizeof($menu_label_translations)){ // if no non-formatted name has been found
                                if(isset($mt) && $mt != ''){$mt = $mt.' - ';} // if there was a page
                                else{$mt = '';}
        
                                // we assign a missing page string to $menu_text
                                $menu_text = $mt.$GLOBALS['t_page_missing'];
                            }
                        }
    
                        elseif($myitems['menu'.$mtp.'_type'] == 'elink'){    // same for elinks but again, we do not need to worry about it for now
    
                            $mt = trim($myitems['menu'.$mtp.'_page']);
    
                            $cv = 0;                
    
                            foreach($menu_label_translations_elink as $mlt){
    
                                if(!in_array($mt,$mlt)){$cv++;}    
                                else{if($mlt[1]== CURRENT_LANG){$menu_text = $mlt[2];}
                            }
                        } 
    
                            if($cv == sizeof($menu_label_translations_elink)){
                                if(isset($mt) && $mt != ''){$mt = $mt.' - ';}else{$mt = '';}
                                $menu_text = $mt.$GLOBALS['t_page_missing'];
                            }
                        }
    
                        elseif($myitems['menu'.$mtp.'_type'] == 'sign'){ 
                                // if row type equals sign (this is the sign-in/out module)
    
                            //if the user is looged in
                            if(isset($_SESSION['c_login']) && isset($_SESSION['c_pwd']) && $_SESSION['c_login'] != '' && $_SESSION['c_pwd'] != ''){
                                // we assign 'signout', internationalized as menu label
                                $menu_text = $GLOBALS['t_'.$myitems['menu'.$mtp.'_type'].'out'];
                            }
                            else{
                                    //otherwise, if not logged in, we assign 'signin', internationalized
                                    $menu_text = $GLOBALS['t_'.$myitems['menu'.$mtp.'_type'].'in'];}
                    }
                    else{ //otherwise, for any other module type

                        if(isset($GLOBALS['t_'.$myitems['menu'.$mtp.'_type']])){
                            // if it exists, we assign the internationalized name of the module to $menu_text
                            $menu_text = $GLOBALS['t_'.$myitems['menu'.$mtp.'_type']];

                        }
                        else{
                                //otherwise we assign a missing module string
                                if($myitems['menu'.$mtp.'_type'] != ''){
                                    $menu_text = $myitems['menu'.$mtp.'_type'].' - '.$GLOBALS['t_module_missing'];
                                }
                                else{$menu_text = $GLOBALS['t_module_missing'];}
                        }
                    } 

                    // if row type equals to registration and the user is logged in, we do nothing
                    if($myitems['menu'.$mtp.'_type'] == 'reg' && isset($_SESSION['c_login'])){} 

                    else{
                            //otherwise, we append the elements (separated by #) of each row (separated by @@) to $this->menu_items, 
                            //so that our feed will be string, not an array.
                            $this->menu_items .= $myitems['menu'.$mtp.'_rank'].'#'.html_entity_decode($menu_text).'#'.$myitems['menu'.$mtp.'_type'].'#'.$myitems['menu'.$mtp.'_page'].'@@';
                            //at this point, our feed is ready for use in menu_tree.php
                    }

                    if(preg_match('/[0-9]#0#/',$this->menu_items)){ //if this menu row corresponds to a top-level menu entry
                        $ct_top_menus++; //we increment the top menu counter by 1
                    }
                }

                if($ct_top_menus == 0){//if no top menus found
                    return $this->menu_items = 'no_top_menu'; //we return just that
                }

                if(isset($mtp) && isset($this->menu_items) && isset($direction) && $direction == 'hmenu'){ 
                    // if the call to this method was issued from hmenu.php, therefore, 
                    // if we need to be able to process our feed in our horizontal layout, 
                    // we first transform it to an array of menu rows

                    $hmenu_exp = explode("@@",rtrim($this->menu_items,"@@"));

                    $menu = array();
                    $line = array();

                    foreach($hmenu_exp as $he){ // and then for each menu row

                        $exp_he = explode("#",$he); //we explode the row

                        array_push($line,$exp_he[0],$exp_he[1],$exp_he[2],$exp_he[3],$exp_he[4]); //we stack everything in a $line array
                        array_push($menu,$line); //and stack each $line in a $menu array
                        $exp_he = array(); //we unset $exp_he and $line
                        $line = array();
                    }
                    krsort($menu); // we sort the $menu array by key
                    $this->menu_items = self::rebuild_menu($menu); //we call the rebuild_menu() method.
                }
            }
            else{    
                    // if menu exists but no row was found in it
                    $this->menu_items = 'menu_empty';
            }
        }
        else{
                //if the menu we seek does not exist
                $this->menu_items = 'menu_missing';
            }
        return $this->menu_items;
        }
    
        public function getTreeMenuItemTranslations($ln, $pfx){ //passing current language and table name as paramaters
    
            MySQLModel::get_mysql_instance()->set_char('latin1');
    
            // creating a query to retrieve all pages or elinks whose language corresponds to the current one
            $q_retrieve_pages = "SELECT * FROM ".$pfx."s 
                                 WHERE ".$pfx."_language = '".$ln."'
                                ";
    
            $this->items_translation_line = array();
            $this->items_translations = array();
    
            MySQLModel::get_mysql_instance()->executeQuery($q_retrieve_pages);
    
                while($mypage = MySQLModel::get_mysql_instance()->getRows($q_retrieve_pages)){ // while we have results
                    array_push($this->items_translation_line, $mypage[$pfx.'_name_formatted'], $mypage[$pfx.'_language'], $mypage[$pfx.'_name']); //we stack them in the $this->items_translation_line array
                    array_push($this->items_translations, $this->items_translation_line); //and stack $this->items_translation_line into $this->items_translations
                    $this->items_translation_line = array(); 
                }
            return $this->items_translations; //and return all processed rows
        }
    
        public function rebuild_menu($menu){ 
            //called inside the getMenuItemsData() method

            $new_menu = array();
            $new_m = array();
    
            foreach($menu as $m){ //foreach menu row
    
                //we rebuild/duplicate the row in a brand new row array called new_m
                array_push($new_m,$m[0],$m[1],$m[2],$m[3],$m[4]);
    
                $kids = self::check_if_kids($m[0],$menu); //and we check if the current row has kids 
    
                //if we found some, we add them to the new row array ($new_m)
                if(sizeof($kids) > 0){array_push($new_m,$kids);}
     
                array_push($new_menu, $new_m); //we stack the new row array into the $new_menu array
                $new_m =  array(); // we empty each new row to avoid redundancy
            }
            return $new_menu; //we return the complete menu set, ready for use in hmenu.php
        }
        
        public function check_if_kids($id, $menu){

            $kids = array();
            $kids_line = array();
    
            foreach($menu as $v){    
                if($id == $v[1]){ //if the current row is a kid to someone, we check whether that kid is not itself a parent 
                    if(sizeof(self::check_if_kids($v[0], $menu)) > 0){ 
                        //if it turns out that that kid indeed has kids
                        ksort($menu); //sorting
                        array_push($kids_line,$v[0],$v[1],$v[2],$v[3],$v[4],self::check_if_kids($v[0], $menu)); // here we use recursion:
                        //we add them to the $kids_line array we're building
                    }
                    else{
                        //otherwise we build our kid line but without adding kids because that kid hasn't any    
                        array_push($kids_line,$v[0],$v[1],$v[2],$v[3],$v[4]);
                    }
                    //and we stack each kid line in the $kid array
                    array_push($kids,$kids_line);    
                    
                    $kids_line = array(); // we override the $kids_line array()
                }
            }
            return $kids; //we return all kids 
        }
    }
    ?>
So basically, the MenuModel class first checks whether the menu we seek does exist, then fetches all its displayable rows (if found any) and builds a thread for each one, based on row type (menu_type). That thread includes, but not limited to, either the localized module name, or, if the module equals to page, the localized non-formatted page name that has been assigned to it. The '#' sign is used to separate row items and '@@' is used to separate rows.

At that point, our feed is ready for use in menu_tree.php, but if the model is being called from hmenu.php, meaning, if we need to be able to process our feed in our horizontal menu, we then first transform it to an array of menu rows, and turn each row to an array of row items, then stack all items into a new $line array which itself is being stacked into a new $menu array. We then rebuild the $menu array using the rebuild_menu() method. This is the method that will restore hierarchy between all rows. It takes a $menu array as sole parameter and for each row traversed, calls upon the check_if_kids() method to simply check if the then-menu row does have kids.

The check_if_kids() method is a recursive one, which, if it finds that the kid it then traverses has kids of its own, adds them to a new row array it is building, all while iterating through those new kids and so on. Otherwise, it does not add anything and is building a regular row array with no kids in it. A resulting kid array is returned to the rebuild_menu() method which again returns us a feed we can use, but in hmenu.php this time.

The show_tables() method allows, based on a database name and a table name pattern, to spot any table that follows that pattern. It is called in getMenuItemsData() and we use it to retrieve all existing menu tables. It is part of the UtilsModel class and appears to be pretty much self-explanatory.
/* from application/UtilsModel.php */

    Class UtilsModel....

    public function show_tables($db_name, $pattern){

        $all_menu_tables = "SHOW TABLES FROM ".$db_name;

        MySQLModel::get_mysql_instance()->executeQuery($all_menu_tables);

        $this->all_table_indexes = array();

        while($tables = MySQLModel::get_mysql_instance()->getRows($all_menu_tables)){
            $table_name = $tables['Tables_in_'.$db_name];    
            if(preg_match('/^'.$pattern.'_.*$/',$table_name)){
                //note the carret sign at the end of the regex, so we don't pick admin_menu_tables alongthe way.
                $exp_table_name = explode("_",$table_name);
                $index = end($exp_table_name);    
                array_push($this->all_table_indexes, $index);
            }        
        }
    return $this->all_table_indexes;
    }
Now is a great time to uncover our tree menu layout, located in menu_tree.php. As stated earlier, it relies on an open-source Javascript/CSS script, which in fact is called dTree, written by Geirs Landrö and attached to this settlement. But between our feed and this script, there is a lot to be done to ensure our tree menu layout is correctly being handled.

Let's take a look at menu_tree.php
<?php
$menu_tree = new MenuModel();
$dmenu = $menu_tree->getMenuItemsData(MENU_TREE_MENU_TABLE,''); //'1','2','3' - leave parameter #2 to avoid extra processing - this type of menu does not need it

if(isset($dmenu)){ 
    
    if($dmenu == 'menu_missing'){
        ?>
        <div class="failure"><?php echo $GLOBALS['t_menu_nb'].MENU_TREE_MENU_TABLE; ?> <?php echo $GLOBALS['t_is_missing_from_db']; ?></div> 
    <?php }
    
    elseif($dmenu == 'no_top_menu'){ 
    ?>
    <div class="failure"><?php echo $GLOBALS['t_menu_nb'].MENU_TREE_MENU_TABLE; ?> - <?php echo $GLOBALS['t_no_top_menu_detected']; ?></div>
    <?php }

    elseif($dmenu == 'menu_empty'){ 
    ?>
    <div class="failure"><?php echo $GLOBALS['t_menu_nb'].MENU_TREE_MENU_TABLE; ?> <?php echo $GLOBALS['t_exists_but_is_empty']; ?></div>
    <?php }

    else{
        
        if(CURRENT_LANG){$ln = CURRENT_LANG;} else{$utils = new UtilsModel(); $ln = $utils->get_default_lang();}
        
        $default_plink = '';

        if(CLEAN_URLS == true){
            if(DEFAULT_PLINK){$default_plink = DEFAULT_PLINK.'/';}
            $home_link = CLEAN_PATH.'/'.DEFAULT_RLINK.'/'.$default_plink.$ln;
            $out = "/out";
        }
        else{
            if(DEFAULT_PLINK){$default_plink = '&'.PLINK.'='.DEFAULT_PLINK;}
            $home_link = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$default_plink.'&'.LN.'='.$ln;
            $out = "&".LETOUT."=yes";
        }
        $home = $_SERVER['PHP_SELF'];

        $rlink = RLINK;
        $plink = PLINK;
        $ln = LN;
        $cur_ln = CURRENT_LANG;
        
        if(isset($_SESSION['c_login'])){$c_login = $_SESSION['c_login'];}else{$c_login = "";}
        ?>
        <table border="0" cellspacing="0" cellpadding="0" class="dtree">
        <div class="dtree">
        
            <script type="text/javascript">
            var home = "<?php echo $home; ?>";
            var homelink = "<?php echo $home_link; ?>"; 
            var home_label = "<?php echo $GLOBALS['t_home']; ?>"; 
            var dmenu = "<?php print(htmlspecialchars($dmenu)); ?>";
            var arosplit = dmenu.split("@@");
            var dmenu2 = '';
            var rlink = "<?php echo $rlink; ?>";
            var plink = "<?php echo $plink; ?>";
            var ln = "<?php echo $ln; ?>";
            var cur_ln = "<?php echo $cur_ln; ?>";
            var c_login = "<?php echo $c_login; ?>";
            var out = "<?php echo $out; ?>";
            var clean_urls = "<?php echo CLEAN_URLS; ?>";
            var clean_root = "<?php echo CLEAN_PATH.'/'; ?>";
                <!--

                d = new dTree('d'); //we create a new tree object called d

                d.add(0,-1,home_label,homelink);  //we add the home node/menu

                 for (i = 0;i<arosplit.length; i++){    //for each menu line    
                     
                    var tlink = "";
                    var olink = "";
                     
                    dmenu2 = arosplit[i];
                    sharpsplit = dmenu2.split("#");
                       
                    if(sharpsplit[3] == "page" || sharpsplit[3] == "elink"){ // if type = page or type = elink
                        if(clean_urls == true){tlink = "/" + sharpsplit[4];}
                        else{tlink = "&" + plink + "=" + sharpsplit[4];}
                    }
                      else{if(sharpsplit[3] == 'signin'){if(c_login != ''){olink = out;}}
                    }               
                    if(clean_urls == true){
                        if(sharpsplit[3] != undefined){ //then add the other nodes
                            d.add(sharpsplit[0],sharpsplit[1],sharpsplit[2], clean_root + sharpsplit[3] + tlink + olink + "/" + cur_ln); 
                        }
                    }
                    else{
                        d.add(sharpsplit[0],sharpsplit[1],sharpsplit[2], home + "?" + rlink + "=" + sharpsplit[3] + tlink + olink + "&" + ln + "=" + cur_ln);
                    }
                  }
                      
                document.write(d); //we print out the tree

                //-->
            </script>
        </div>
        </table>
        <?php unset ($plink);
    }
}
?>
So, from the moment we drew our menu feed to store it inside $dmenu, we first echo missing menu messages and alike, if any, then we take care off the default language, default page link and start building our home link since we're not yet looping through the menu rows we have. Note that only the tree menu will have a home link feature, proper to itself. It will of course be possible to add a home link to the horizontal menu, but it will have to be a regular type of link. We then start gathering all the PHP variables we need and pass them to Javascript to help build the menu nodes(rows). We instantiate a tree using the dTree() method from the third-party tree menu library we use and start adding the home link by invoking the add() method of that library. Then, looping through each menu row, we dynamically build and add the rest of the nodes. Finally, we display the resulting tree on screen. Again, the proper dTree Javascript/CSS library files would be provided as attachments. Note that these libraries have been modified to support clean URL's and also so that you can bring you own icons and style along the way.

Last, but not least, hmenu.php.
<?php
$hmenu = new MenuModel();
$menu = $hmenu->getMenuItemsData(HMENU_MENU_TABLE,'hmenu'); // Leave 'hmenu' as is, to have the db result set undergo extra processing to make it fit with this kind of menu.

if(isset($menu) && is_string($menu) && $menu == 'menu_missing'){ 
    ?>
    <div class="failure"><?php echo $GLOBALS['t_menu_nb'].HMENU_MENU_TABLE; ?> <?php echo $GLOBALS['t_is_missing_from_db']; ?></div>
<?php }
if(isset($menu) && is_string($menu) && $menu == 'menu_empty'){ 
    ?>
    <div class="failure"><?php echo $GLOBALS['t_menu_nb'].HMENU_MENU_TABLE; ?> <?php echo $GLOBALS['t_exists_but_is_empty']; ?></div>
<?php }
if(isset($menu) && is_string($menu) && $menu == 'no_top_menu'){ 
    ?>
    <div class="failure"><?php echo $GLOBALS['t_menu_nb'].HMENU_MENU_TABLE; ?> - <?php echo $GLOBALS['t_no_top_menu_detected']; ?></div>
<?php }

if(isset($menu) && is_array($menu)){
?>
<ul class="hmenu" id="hmenu">
<?php
$letout = ''; 

foreach($menu as $m){
 //foreach menu line    
    $child = $m[0];
    $parent = $m[1];
    $translation = $m[2];
    $type = htmlspecialchars($m[3]);
    $page = htmlspecialchars($m[4]);

    if($type == 'page' && $page != '0'){
        if(CLEAN_URLS == true){$pg = '/'.$page;}
        else{$pg = '&'.PLINK.'='.$page;}
    }
    elseif($type == 'elink' && $page != '0'){
        if(CLEAN_URLS == true){$pg = '/'.$page;}
        else{$pg = '&'.PLINK.'='.$page;}
    } 
    else{$pg = '';}
     
    if($type == 'signin'){
        if(!isset($_SESSION['c_login']) || $_SESSION['c_login'] == ''){
            //$type = 'signin';
            $letout = '';
        }
        else{
            //$type = 'signin'; 
            if(CLEAN_URLS == true){$letout = '/out';}
            else{$letout = '&'.LETOUT.'=yes';}
        }
    }
//if(isset($m[5])){sort($m[5]);}
    if($parent == '0'){
        //IF TOP MENU, WE DISPLAY IT
        if(CLEAN_URLS == true){$url = CLEAN_PATH.'/'.$type.$pg.$letout.'/'.CURRENT_LANG;} 
        else{$url = SITE_URL.'?'.RLINK.'='.$type.$pg.'&'.LN.'='.CURRENT_LANG.$letout;}
        ?>
        <li>
            <a href="<?php echo $url; ?>" class="hmenulink"><?php echo $translation;
            
            if(isset($m[5]) && sizeof($m[5]) > 0){// if the topmenu has kids, we add a decorative arrow
                ?>&nbsp;<img src="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES.'/'.CURRENT_THEME; ?>/images/servicing/hmenu/down-arrow.png" width="9" height="5" />
        <?php } ?></a>

        <?php
            if(isset($m[5]) && sizeof($m[5]) > 0){//and we call the get_kids() function to display them
                $ml = 0;
                ksort($m[5]);                        
                echo get_kids($m[5],$child, $ml);             
            }
        ?>
        </li>
        <?php
        }
    }
    ?>
</ul> 
<script type="text/javascript">
    var hmenu = new hmenu.dd("hmenu");
    hmenu.init("hmenu","menuhover");
</script>
<?php 
}

function get_kids($kid, $child, $ml){

    krsort($kid); //to test
    $letout = '';
    
    $feed = '<ul class="hmenu" id="hmenu">';    

    foreach($kid as $k){
         //for each kid
            
        $par = $k[1];
        $ch = $k[0];
        $tr = $k[2];
        $ty = htmlspecialchars($k[3]);
        $pag = htmlspecialchars($k[4]);

        if($ty == 'page' && $pag != '0'){
            if(CLEAN_URLS == true){$pg = '/'.$pag;}
            else{$pg = '&'.PLINK.'='.$pag;}
        }
        elseif($ty == 'elink' && $pag != '0'){
            if(CLEAN_URLS == true){$pg = '/'.$pag;}
            else{$pg = '&'.PLINK.'='.$pag;}
        } 
        else{$pg = '';}

        if($ty == 'signin'){
            if(!isset($_SESSION['c_login']) || $_SESSION['c_login'] == ''){
                //$ty = 'signin';
                $letout = '';
            }
            else{
                //$ty = 'signin';
                if(CLEAN_URLS == true){$letout = '/yes';}
                else{$letout = '&'.LETOUT.'=yes';}
            }
        }
                
        if($par == $child){
            if(CLEAN_URLS == true){$url = CLEAN_PATH.'/'.$ty.$pg.$letout.'/'.CURRENT_LANG;}
            else{$url = SITE_URL.'?'.RLINK.'='.$ty.$pg.'&'.LN.'='.CURRENT_LANG.$letout;}

            $feed .= '<li>';
            $feed .= '<a href="'.$url.'" class="shmenulink2">'.$tr;
        
            if(isset($k[5])){// if kids, we add an arrow
                $feed .= '&nbsp;<img src="'.CLEAN_PATH.'/'.PATH_TO_THEMES.'/'.CURRENT_THEME.'/images/servicing/hmenu/down-arrow.png" width="9" height="5" alt="'.$tr.'" /></a>';
            }
    
            if(isset($k[5])){$feed .= get_kids($k[5],$ch, $ml);}    //recursion
            $feed .= '</li>';
        }
    }
    $feed .= '</ul>';
    return $feed;
}
?>
For the most part, hmenu.php, as highlighted in the comments, does just what menu_tree.php does, but there are important differences though. Its layout is partly being drawn here, as opposed to menu_tree.php, which has its layout being entirely drawn inside the dTree library. Most importantly too, this is here we will use recursion while looking for kids.

In hmenu.php, if we find a top row, we display it, and if that top row has kids, we not only add a decorative arrow ourselves, but we do call the get_kids() function, which, since its a recursive one, will, upon finding new kids, call itself back. Once the loop is complete, we call on our third-party horizontal library to create a menu object called hmenu, passing our new structure as parameter, and initialize that object passing the relevant CSS class names as parameters.

So now, we have a fully functional front-end menu system working for us, which offers a choice of two layouts, vertical and horizontal. Both support an infinite number of levels and are totally customizable by means of CSS and Javascript too. This menu system will now on allows us to hierarchically present our pages and modules nicely, without having the users hard-typing their URL in their browsers. In a future settlement, I will show you how to build the back-end management module that will allow the creation/edition/deletion of those menu rows as well as the menus itself.

But next time, we will set our focus on how to handle the user experience by creating a sign module along with a user profile module, that will allow the user to get into its personal account interface and update personal data.

This article first appeared Sunday the 28th of December 2014 on RolandC.net.