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.

How to build your own Multilingual PHP MVC CMS from scratch - Part 3 - Frontend (part 1): Page Module

Wednesday, November 5, 2014

Welcome to the third settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which we will be creating our first entire module, the page module.

Last time, we have set the focus on the index.php file, responsible for routing user input, calling all the needed core components that we have previously reviewed. 
We have seen how the Router class, which can be assimilated as a factory, once instantiated and invoked by means of its own route() method, generates and triggers, based on user input, a new controller object that itself, aided by the __autoload() function from the index file, calls on the name-matching module class it needs in order to get all the logic and data it will use to build the needed view and it does so by relying on the $registry object that was passed onto it while being instantiated by the $router object, the $registry object containing a template object, itself containing a method to include the needed view file as well as a __set() method to store all the template variables the controller needs to pass to the view file, in order to be used by the programmer there.

So today, we are going to take a dive straight into the page module, see how its  controller, model and view really work on the ground. But before we start with the page controller class, let's take a brief look at the structure of the database table called 'pages', which contains all the data we will rely on to effectively handle pages in our CMS.
 --  
      -- Table structure for table `pages`  
      --  
      CREATE TABLE IF NOT EXISTS `pages` (  
      `id_page` smallint(6) NOT NULL,  
       `page_data` mediumtext,  
       `page_name_formatted` varchar(60) NOT NULL,  
       `page_name` varchar(80) NOT NULL,  
       `page_language` varchar(2) NOT NULL,  
       `page_created` datetime DEFAULT CURRENT_TIMESTAMP,  
       `page_last_edited` datetime DEFAULT CURRENT_TIMESTAMP  
      ) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;  
      --  
      -- Indexes for dumped tables  
      --  
      --  
      -- Indexes for table `pages`  
      --  
      ALTER TABLE `pages`  
       ADD PRIMARY KEY (`id_page`), ADD FULLTEXT KEY `pages_data` (`page_data`);  
      --  
      -- AUTO_INCREMENT for dumped tables  
      --  
      --  
      -- AUTO_INCREMENT for table `pages`  
      --  
      ALTER TABLE `pages`  
      MODIFY `id_page` smallint(6) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=0;  
      /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;  
      /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;  
      /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;  

Note that the field called page_name_formatted, since it corresponds to the formatted name of the page that will be either be displayed in the URL or serve inside menu links, to play it safe, will always need to contain nothing but lowercase, latin-alphabet letters and/or figures, with only underscore/minus signs allowed. Also, page_language will always have to contain two lowercase letters only.

Here is a dummy data sample for you to insert in your own page table
 INSERT INTO 'pages' ('id_page', 'page_data', 'page_name_formatted', 'page_name', 'page_language', 'page_created', 'page_last_edited') VALUES  
      (1, 'data for page 1', 'page_1', 'Page 1', 'en', '2014-09-06 23:05:39', '2014-09-06 23:53:19'),  
      (2, 'data for page 2', 'page_2', 'Page 2', 'en', '2014-09-06 23:06:42', '2014-09-06 23:55:28'),  
      (3, 'data for page 3', 'page_3', 'Page 3', 'en', '2014-09-06 23:07:18', '2014-09-06 23:58:02')  
      );  

So now, without further waiting, here comes our PageController class
 <?php  
      /* application/controllers/PageController.php */  
      class PageController extends BaseController{   
           //Declaring our PageController class  
           //This class benefits from the properties of the abstract parent class it extends, called BaseController,   
           //but even though the parent class forces us to have an initialize() method set here,   
           //it is up to us programmers to decide how we wish to implement it here.  
           //declaring our private properties  
           private $module_name; //passed to main.php to validate the including of page.php  
           private $page_data; // passed to page.php as containing the actual data stored in the DB for the particular page the user seeks   
           private $page_name; //passed to header.php to serve inside the meta title tag  
           private $page; // only this property will not be passed directly to the view,   
           //instead, it will hold the value of plink and be passed as a parameter to the model's get_data() method   
           //Note: those properties will not be passed as-is to the view, not from here at least, so we can confidently keep them as private while here.  
           public function initialize(){ // this is the method our abstract BaseController class forces us to declare  
                //Next we issue a few calls to some methods that will in turn call upon the model to get the needed data  
                //and we store the results as properties of the template object (remember the Template class contains a __set() method too)   
                //The template object is being itself stored as a property of the registry object  
                $this->registry->template->module_name = $this->getModuleName();  
                $this->registry->template->page_data = $this->getPageData();  
                $this->registry->template->page_name = $this->getPageName();  
                //note the $this keyword which specifies that we refer to this class only  
                //finally, we call the assign_theme() method from the template object,   
                //that method will take care off preparing the above properties for display,   
                //plus it will include our common template files (header, main and footer)  
                $this->registry->template->assign_theme();  
           }  
           public function getModuleName(){  
                // we instantiate our PageModel class, which is Singleton, so we need to use the :: syntax and call its getInstance() method to do so.   
                $model = PageModel::getInstance();  
                //and we simply call its get_module_name() method, which will simply return the name of the module, as a string.  
                return $model->get_module_name(); //We return than name  
           }  
           public function getPageName(){  
                // same operation with the get_page_name() method, which will return   
                // the non-formatted localized name of the page from the DB  
                $model = PageModel::getInstance();  
                return $model->get_page_name();  
           }  
           public function getPageData(){  
                if(CURRENT_PLINK){$this->page = CURRENT_PLINK;}  
                else{$this->page = DEFAULT_PLINK;}   
                // we assign whatever plink value we have fetched in the index   
                // otherwise we assign the default plink value  
                $model = PageModel::getInstance(); //we instantiate our PageModel class again   
                return $model->getData($this->page,CURRENT_LANG);   
                // and call the getData() method (passing the URL page name and current language as parameters),  
                // which will fetch the page content from the DB for us. We return the result of the call  
           }  
      }  
      ?>  
We start by declaring our PageController class and declare the few properties we will need along the way. Only the top three properties will be passed to the view.

Then we call the initialize() method, the top three properties: module_name, page_data and page_name are being passed to the view, as they are being assigned the results of specific method calls to the PageModel object, by being added to the template object, which itself had been added to the registry object as a property and which also contains a __set() method allowing it to store the properties to be then passed to the view.

Finally we call the assign_theme() method from the template object. That method will prepare the above properties for display and it will include our common template files as well: header.php, main.php and footer.php.

Then remains the actual calls to some of the PageModel's methods. Those in this particular case are the calls that feed the values of our above properties, the ones we want to pass to the view. The PageModel class is a Singleton one and so we need to access and instantiate it by using the :: syntax, plus by making a call to its public and static getInstance() method. The calls to our model's methods are not made directly though. While still inside the initialize() method, we issue a call to a dedicated controller method that will itself invoke the needed model's method.

So now has come the time to lay our PageModel class down on the table.
 <?php  
      /* application/models/PageModel.php */  
      class PageModel{ //declaring our PageModel class  
           static $instance;     // needed to help make the class a Singleton one  
           private $module_name;       
           private $pageData;  
           private $page_name;  
           public static function getInstance(){ //method to create a single static instance of this class,   
                //which will be accessed from anywhere outside the class using the :: operator.   
                //It can be called anything you wish.  
                if(self::$instance == null){ // if no instance was created  
                     self::$instance = new self(); // we create one   
                     //the keyword 'self' reffers to the current class  
                }  
                return self::$instance; //returning the newly created instance  
           }  
           private function __construct(){}   
           private function __clone(){}  
           //Those two methods are here to prevent the programmer from creating a second instance of this class by using the new() or clone() methods.   
           //Only one instance of this class will be accessible and it will be accessible using the getInstance() method only.   
           //This is how Singleton classes are built.  
           public function get_module_name(){ //the method that will set and return the module name we need in main.php   
                //If $module_name has been set and passed to main.php, then page.php will be included   
                //in main.php, where we need it to graphically appear.  
                $this->module_name = 'Page'; //setting the value of the module_name property,   
                //which has not yet been turned to a variable by the template object   
                return $this->module_name; //returning that value  
           }                 
           public function get_page_name(){   
           //method to get the non-formatted, localized name of the page  
                if(CURRENT_PLINK){   
                     // if plink has been set in the URL and is not empty  
                     //we instantiate MenuModel (also a Singleton)  
                     $menu_instance = MenuModel::getInstance();  
                     $page_list = $menu_instance->getTreeMenuItemTranslations(CURRENT_LANG,'page');   
                     //and fetch the list of all existing pages found in DB 'pages' table.   
                     //This is a method that originally serves other purposes,   
                     //but since part of the data it returns contains all the data we need here,   
                     //we save ourself writing an extra method by using it.  
                     $cv = 0; //we initialize a counter  
                     foreach($page_list as $pl){ foreach known front-end page  
                          if(!in_array(CURRENT_PLINK,$pl)){$cv++;}   
                               //if our page name cannot be found inside the currently   
                               //looped page from the DB, we increment $cv by 1.  
                          else{   
                                    //otherwise, that means our page name is valid  
                                    if($pl[1] == CURRENT_LANG){$this->page_name = $pl[2];  
                                    //and once the current language is being matched, we assign   
                                    //the corresponfing non-formatted page name to the page_name property  
                               }  
                          }  
                     }   
                     if($cv == sizeof($page_list)){ // if the page name could not be found while looping the array above,//we build a missing link instead and assign it to the to the page_name property  
                          $this->page_name = CURRENT_PLINK.' ('.$GLOBALS['t_'.CURRENT_LANG.') - '.$GLOBALS['t_page_missing'];  
                     }  
                }  
                else{$this->page_name = ''; //if no page name was set, we assign it an empty string value}  
                return $this->page_name; //we return the page name, whatever its value  
           }       
           public function getData($page, $ln){ //we declare the function, taking page name and language as parameters  
                $this->pageData = array(); //we declare the array that will hold our page data from the DB  
                $page = filter_var($page,FILTER_SANITIZE_MAGIC_QUOTES); //we use the PHP native filter_var function to sanitize our page name.   
                //This is primarily a matter of security as we don't want issues related to SQL Injection attacks   
                //by means of manually (deliberately?) placing quotes or double quotes in the URL to or in place of the page name.  
                MySQLModel::get_mysql_instance()->set_char('latin1'); //we invoke our MySQL library's set_char() method   
                //to avoid encoding issues with the content we pull here. Latin1? yes, more on that in a future settlement though.   
                $my_query = "SELECT id_page, page_name, page_data   
                              FROM pages WHERE page_name_formatted = '".$page."'   
                              AND page_language = '".$ln."'  
                            ";   
                //we build a query in which we select all entries of which the formatted named and language correspond respectively to our current page and language names. Only one single row should be found as a result.  
                MySQLModel::get_mysql_instance()->executeQuery($my_query);  
                // we instantiate our MySQL instance and execute the query  
                $res = MySQLModel::get_mysql_instance()->getRows($my_query);  
                // and we pull what's been found, assign it to $res  
                if($res != ''){ //If what's been found isn't empty  
                     array_push($this->pageData, $res['page_data'], $res['page_name']); //we stack it insde the pageData array  
                }            
                else{ //otherwise  
                          if(isset($page) && $page != ''){ $page = stripslashes($page).' ('.$GLOBALS['t_'.$ln].')';}   
                          else{$page = '';}   
                          //we stack a missing page message in the pageData array,   
                          //that will include the image file name of the missing page sign we will display in the view.  
                          array_push($this->pageData, $page.$GLOBALS['t_page_missing'],'missing_page2.png');  
                }  
                return $this->pageData; //and we return whatever we have picked up and stacked in the pageData array.  
           }  
      }  
      ?>  
The PageModel class is a Singleton one. I have described in the comments the way a Singleton class can be built, by declaring a public static function that will create a single static instance of the class every time it will be called. But, to remain a Singleton, the class needs to be declared empty __construct() and __clone() methods so that a second instantiation cannot be initiated by the programmer, even mistakingly.

The get_page_name() method checks if page name (CURRENT_PLINK) is set and not empty, then pulls a list of all existing pages and sees if it finds it there. If it does, it assigns the page's non-formatted and localized name to the page_name property, and if it does not, it assigns a missing page message to that same property. And if page name is not set, it assigns an empty string to page_name. For the sake of this tutorial however, you may skip checking with the existing page list and simply return the value of CURRENT_PLINK, provided it is set and not empty. 

Now, in the getData() method, we set the page_data property as an array, modify the database charset to 'Latin1' and sanitize the $page parameter from the method call using PHP's native filter_var() function. Then we build a query string to pull the page table entry that matches our current page and language names and simply execute that query. In the page_data array, we stack the fields we need from the freshly pulled results. And if results there aren't, we stack a missing page message in it, along with the name of the missing page image file we will display with the message. 

The page model contains fairly simple methods and its role can mostly be summed up as simply pulling content from the page table, based on page name and current language. It seems therefore fair to warn the reader on that most of the models we will soon be dealing with are not that simple in content and/or architecture. But have no fear, as we have about reviewed the core structure of the architecture we're going to be evolving in, from now the difficulty level should not be increasing that significantly. However and by nature, some of the models we will be reviewing soon will present algorithms of which the explanation might not appear to be so obvious to all readers at first. In any a case, both code comments and explanations will make up for it, so that you will never really have to worry about the growing complexity of the project.

Back one level up in our page module hierarchy, now that we have pulled the page data we needed from the DB and returned it to the controller, we're all set to call the assign_theme() method, which will transform some of our controller's properties to regular variables/arrays we can actually use inside the theme files. 

Let's first take a look at a sample from main.php
 <?php  
      /* themes/your_theme_name/main.php */  
      ...   
      <td>  
           <?php  
                try{     //we do use a try/catch bloc with this kind of include  
                          if(isset($module_name) && !empty($module_name)){   
                                    //if the controler has passed us the module_name property   
                                    //and provided it's not empty, we then use it to include the corresponding view file  
                                    $path = PATH_TO_THEMES.'/'.CURRENT_THEME.'/'.strtolower($module_name).'.php';  
                                    include $path;  
                          }  
                          else {  
                                    //else we throw an exception containing whatever message we need to visually display  
                                    throw new Exception('<p>&nbsp;</p><p>&nbsp;</p><div class="loading_tpl_fails">'.$GLOBALS['t_the_theme_named'].' <u>'.$module_name.'</u> '.$GLOBALS['t_couldnt_be_found'].'</div>');  
                          }  
                }  
                catch(Exception $e){  
                     echo $e->getMessage();  
                     exit(0);  
                }  
           ?>  
      </td>  
      ...  
      ?>  
The main.php file is the place where page.php will actually be included, as again, it will not be included straight from the assign_theme() method of the template object. We do use a try/catch bloc to wrap this include statement.

And now, let's see what page.php has to tell us
 <?php  
      /* themes/your_theme_name/page.php */  
      <?php  
      // See how we find back our controller properties here,   
      // transformed to regular variables or arrays, holding the value(s) we have assigned them  
      // while still in the initialize() method of the controller  
      if(isset($page_data[1]) && preg_match('/missing_page/',$page_data[1])){ //if the required page is missing  
      ?>   
           <img src="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES.'/'.CURRENT_THEME.'/'; ?>images/servicing/<?php echo $page_data[1]; ?>" />  
           //we display a missing page image (remember we stacked the image   
           //file name inside the page_data array while in the model   
           //then returned it to the controller and passed it onto the view here)  
           <p>&nbsp;</p>  
           <?php  
           }  
           if(isset($page_data[0]) && is_string($page_data[0]) && !preg_match('/missing_page/',$page_data[0]) ){   
                //if the required page is not missing and its value does not match 'missing_string'  
                ?>  
                <div align="center">  
                <?php   
                     print $page_data[0]; //we print the data we had stacked in the page_data array  
                ?>  
                </div>  
           <?php   
      }  
      ?>       
      <br />  
      ?>  
We can now clearly see how we find back and use our controller properties while inside the view, eg: page_data, transformed to regular, usable PHP array, holding the value(s) we assigned it while we were still in the initialize() method of the controller.
As for the algorithm of the overall page template, it is very simple: if the required page is missing, we display a missing page image with a missing page string underneath, otherwise, we display the content we have pulled from the DB for the required page.

By now and by having gathered all code from this part as well as from the last one, referring to the first part for directory structure information too, you should already have a basic application that allows you to display a page based on URL input. For now though, we have only been discussing the page module.

But as an example, loading http://localhost/your_project_name/index.php?q=page&page=page_3&ln=en into your browser, if successful, should serve your screen three template files, namely header.php, main.php and footer.php, with main.php containing page.php, itself containing the DB-pulled content for page_3 (provided page_3 is not missing from the DB). If page_3 is missing from the DB, a missing image will be displayed and you are free to choose an image for yourself to work with. The page.php file can be placed everywhere it would graphically makes sense to you, as long as it stays inside main.php. 

Note that for clarity reasons I have not been including header.php and footer.php in the series yet but for now, you can make up for it by adding some simple header and footer files, header.php file containing the usual html, header tags we all know about. This is where you will link your main theme CSS file, and if your style name is called pinguin, then your main CSS file would have to be called pinguin.css. The favicon file will be stored in /themes/your_style/images/favicon and be called favicon.ico. The main.php file will contain the first body tag and footer.php will contain the closing body and html tags. This way, you can start effectively styling your project while we're getting advanced but still haven't reviewed all the theme-handling aspects of our project yet. Those will be the subject of a whole forthcoming settlement.

Last but not least, in case this wasn't implicit enough: every time you encounter a global variable of the form : $GLOBAL['t_variable_name'], this means, by the very standards of this project, that this is a translation variable and that basically, in the language file pertaining to the current language (languages/current_language/current_language.php) should exist a variable called $t_variable_name = 'Translation string';. Otherwise a missing variable error is being thrown by PHP. Again, language-handling will be discussed in more details in a forthcoming settlement.

Meanwhile, do not hesitate to refer to your copy of Gumbo you have probably downloaded already last week. This will hopefully help you solidify your understanding of the project we're building together. 

So this completely wraps up this settlement on how the page module works, bottom up. Next time, we will get our hands tied on yet another generally essential - not to say vital - feature in Web development, the menu system.

This article first appeared Tuesday the 5th of November 2014 on RolandC.net.


How to build your own Multilingual PHP MVC CMS from scratch - Part 2 - Starting Point: index.php

Wednesday, October 29, 2014

Welcome to this second settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which we will be discussing the entry point of our application: The index.php file.
During the first part, we have, among other things, reviewed the core principles around which we will develop our project and also briefly introduced its directory structure.
But now, all those would be totally useless if not for one single file, the one and only entry point of our application, the index file, responsible for handling user input, requiring all the needed components along the way and which we are now going to decorticate, step by step.


How to build your own Multilingual PHP MVC CMS from scratch - Part 2 on RolandC.net
Gumbo-CMS is the application on which these series are based


Before we start: I have based these series entirely on Gumbo-CMS, the new Multilingual PHP5 MVC CMS in town, which I have recently released under the GPL V2 License. So feel free to download yourself a copy before we actually get started, as it will contain every single snippet of code you will be able to find throughout the entire tutorial. This will provide you with some guidance if you ever get lost during one of the forthcoming settlements. Equally, you will find useful insights as to which kind of feature Gumbo-CMS offers by taking a look at its Product Overview PDF.

So, we jump right in by declaring a session on line 2, just below the opening php tag and we also include our config file.
 <?php  
      //we initiate a session, useful for user sessions handling,
      // last visited URLs, etc.
      session_start();   
      //and we include our config file
      include 'config.php';   
Next, we instantiate the UtilsModel class, which contains a few useful 'all-purpose' functions.
 // we instantiate our general-purpose class called UtilsModel
 $utils = new UtilsModel();  
Then we define our timezone.
 date\_default\_timezone_set(DDTS); // we define our timezone  
DDTS will be the config constant that holds our timezone value, eg: Europe/Stockholm, Europe/Paris. This is required since PHP 5.10 because otherwise, I quote from the php.net site : 'every call to a date/time function will generate a E_NOTICE if the timezone isn't valid, and/or a E_WARNING message if using the system settings or the TZ environment variable'. Alternatively, you can also set the date.timezone parameter in php.ini.
 /*recording the last visited url*/  
      if(isset($_GET[RLINK]) && $_GET[RLINK] != 'signin' && $_GET[RLINK] != 'fpwd' && $_GET[RLINK] != 'fpwd'){  
           //if a module is set and that is neither the signin model nor the forgotten password one, we store the request URI in a session variable  
           $_SESSION['last_url'] = $_SERVER['REQUEST_URI'];  
      }  
This above is a snippet that pertains to the logic of our CMS, in regard to the recording of the last visited URL. This is also a good occasion to start introducing the URL constant naming scheme we will be using throughout the whole project while developing it.

RLINK, implicitly meaning Router Link, is the constant that holds the $_GET name of a module name, be it current or sought after. Equally important are PLINK and LN, which will respectively hold page and language names.

The value of all URL constants can be modified to taste, as long as the CLEAN_URLS constant is set to false. This because of the rewrite rules in .htaccess that will not allow the dynamic naming of target variables within its rules. But as long as clean URLS aren't enabled, you are free to give any value you wish to your URL $_GET variables.

Let's break away momentarily from the index.php file to take a look at this extract from config.php
 /* config.php */  
      ...  
      define("CLEAN_URLS",false); //true or false. true to enable it, false to disable - NOTE : mod_rewrite has to be enabled for it to work. We will set it to true when we reach our settlement about setting up clean URL's, so for now, let's keep it disabled  
      /*routing*/  
      define("RLINK","q"); //Router Link- var that holds the current module name in $_GET requests - make sure it's different from the other url variables found in this file, and different from google cse reserved terms (cx, language, sa, etc...) - use 'ctrl + F' to check within this file  
      define("DEFAULT_RLINK","page");          // Warning: if you choose a default module that's different from the page module,  
      define("DEFAULT_PLINK","formatted_name_of_the_page_you_wish");     //Page Link     // then you will have to leave the DEFAULT_PLINK value blank;  
      define("PLINK","p");  
      ...  
But back to our little snippet in the index (commented as 'recording the last visited url'), what it says is: If a module is set and that module is different from the signin module and different from the forgotten password module too, we then store the REQUEST URI in a session variable we call $_SESSION['last_url']. We do this because we simply don't want to have the user automatically come back to the login screen or the forgotten password procedure once it has successfully logged in.

And now has come the time to call and instantiate some of our core classes, namely the router, registry and template classes.
 /* requiring the core clases */  
      require_once('application/Router.php');   
      require_once('application/Registry.php');  
      require_once('application/Template.php');  
      /* instantiating them */  
      $router = new Router();  
      $registry = new Registry();  
      $registry->template = new Template(); // here we add the new template object to the registry object, as one of its properties  
The router will do the work of routing user input by retrieving the requested GET variable, then including and calling the corresponding controller, based on the value of that variable. We will review what the router actually does in detail when the route() function is called later in the the file.

We call on the registry, which contains magic __set() and __get() methods: this will simply provide a registry to our application in which to store some of our application variables.

We also call and instantiate the template class, adding it to our registry object as one of its properties and which contains our template engine. It will store both the variables assigned by the controller for the view to use and include the view itself.
 /* DB connection initiating */  
      MySQLModel::get_mysql_instance()->newConnection(DBHOST,DBUSER,DBPWD,DBNAME,DBPORT);   
      // we instantiate our Singleton MySQLModel class and invoke the newConnection() method, passing our config's db settings as parameters  
To connect to and interact with the MySQL database, we will use a custom MySQLModel class, which I have found on the Internet and turned into a Singleton class, so its instantiation gets restricted to one object only and which we will be able to access via the get_mysql_instance() method, every time we need to perform an operation with the database.

Singleton? Yes. Design patterns, in the software development industry, are patterns that have been observed, tried and accepted as being the most likely solutions to a set of commonly encountered problems, so that you do not have to reinvent the wheel and implementing solutions to commonly encountered patterns is somehow the best way you can use to make sure you're being on the right track, at least on the same track as most others.

The Singleton pattern is a very popular software design pattern that's very useful in our case. Our MySQLModel class does not need and shall not be instantiated more than once, all while it will still be able to perform its duties CMS-wide.

Having made our MySQLModel class a Singleton one allows us, once loaded, to access it from anywhere using the :: syntax, thus we do not have to store our database connection inside a global variable, a session variable or whatever else.

Using the MySQLModel::get_mysql_instance()->newConnection(params) syntax, we simply instantiate our class and add a call to the newConnection() method altogether, passing our database connection constants from the config file as parameters. Since the call is made early enough in our index file, we do not need to perform any additional connection anywhere else. We will only need to use the MySQLModel::get_mysql_instance()->method_name(params); syntax every time we need to perform an operation with MySQL.

Note that even though we have named our class the MySQL model, we do connect to MYSQL via the mysqli extension.

Next, we engage in pre-routing user input, or what remains of it. Up to Gumbo-CMS v0.96beta (included), pre-routing was operated by means of header redirects, under the logic that we always wanted to have a valid module link (RLINK) and a valid language link (LN) defined at a minimum in the URL, at any time.

Now bare with me, Google does not like header redirects, even though they claim 302 status is fine for them, so you will have all the trouble of the world trying to index your site using such (security?) procedure. While the Bing engine will index your code just fine, Google's spiders will not because they see these kinds of redirects as hijacking attempts. Which is why v.97 features a new pre-routing approach which does not use PHP headers at all.

In this new approach, we will simply determine a value for PLINK, RLINK and LN and assign those to constants, respectively CURRENT_PLINK, CURRENT_RLINK and CURRENT_LANG
 /* pre-routing (or what's left of it) */  
      
      $plink = ''; 
      // if there is a $_GET[PLINK] set, let's use it
      if(isset($_GET[PLINK])){$plink = $_GET[PLINK];}
      // otherwise let's use DEFAULT_PLINK
      else{$plink = DEFAULT_PLINK;}
      // whatever what set, we assign it to CURRENT_PLINK, to use it wherever needed, whenever needed
      define("CURRENT_PLINK",$plink);  
      
      $rlink = '';  //same here for RLINK, with the difference that :
      // we use a module name list stored in config.php to check if that 
      // if we have a module set in URL, it has to be a valid module name
      $exp_menu_type_list = explode(";", MENU_TYPE_LIST);  
      if(isset($_GET[RLINK]) && in_array($_GET[RLINK], $exp_menu_type_list) ){$rlink = $_GET[RLINK];}  
      // otherwise, we use the default RLINK value, namely DEFAULT_RLINK 
      else{$rlink = DEFAULT_RLINK;}
      // same as for PLINK, we store what we have in a constant  
      define("CURRENT_RLINK",$rlink);  
      
      $dlang = $utils->get_default_lang(); //we need to fetch the default language 
      $available_languages = LocModel::getInstance()->getData_language_bar(); // we also need a list of the currently existing language in the database 
      
      // Since the available language list we've just fetched contains more than what we need
      // we create an array in which we will store just the elements we seek 
      $available_language_codes = array();   
      foreach($available_languages as $a_l){array_push($available_language_codes, $a_l[0]);}  
      
      // Now, if a language is set in the URL, AND is a valid language (made of only 2 lowercase letters) 
      // AND can be traced in our available language codes array we've just filled in, 
      // then it's all a valid language that we will store in $langf
      if(isset($_GET[LN]) && preg_match('/^[a-z]{2}$/',$_GET[LN]) && in_array($_GET[LN], $available_language_codes)){$langf = $_GET[LN];}
      // else if Google's language URL variable is set (we will have to rely on it while using the search module)
      // we use it  
      elseif(isset($_GET['language']) && preg_match('/^[a-z]{2}$/',$_GET['language']) && in_array($_GET['language'], $available_language_codes) ){$langf = $_GET['language'];}
      // else if a session language variable is set, we use it
      elseif(isset($_SESSION['C_LANG'])){$langf = $_SESSION['C_LANG'];}       
      // else we assign the default language we fetch a little earlier
      else{$langf = trim($dlang);}  
      // and define a CURRENT_LANG constant to assign it whatever value we have just fetched 
      define("CURRENT_LANG",$langf);
  
      // we create a language session variable, that will help in cases when a language was selected other than the default one and the user/visitor reloads the home link with no parameter, so the current language is not set back to default.
      $_SESSION['C_LANG'] = $langf;
  
So, here we first take care off PLINK - if it wasn't set, we use the default value found in config.php (DEFAULT_PLINK), assign what we have to CURRENT_PLINK, that we create.

We do just the same for RLINK, except we will confront it to a menu type list found in config.php under the name of MENU_TYPE_LIST and which contains all the valid module names in use on the front-end part. If a module is set in the URL and can be found in the menu list, we then assign it to $rlink. Otherwise $rlink takes the default rlink constant value, DEFAULT_RLINK. We again create a constant to hold that value, which we name CURRENT_RLINK.

LocModel, also Singleton, is a very useful class that contains a few methods that will help us manage languages effectively. We will be using it here to fetch all the available languages.

We then fetch the default language and store it into $dlang and we then also fetch a list of our available languages, which we process to a new array called $available_language_codes.

Now, if a language was set in the URL and is found valid, by format and reference to our existing language list, we do use it.

Else, if a Google-owned language URL parameter is set (namely $_GET['language']), we will use it instead. This will happen only when using the search module (powered by Goggle CSE), while in two-column view.

Else again, if we have a language session variable set, we make use of it, otherwise, and only otherwise, we try the default language that we have fetched a little earlier

In any case, passed that point, we do have a valid module name, page name (empty or not), and language name, so that, we can route the proper translation files, using a try/catch bloc as follows :
 /* routing translations now */  
      try{  
           $ln_file = PATH_TO_LANGUAGES.'/'.CURRENT_LANG.'/'.CURRENT_LANG.'.php';  
           if (file_exists($ln_file)){include ($ln_file);} // if our language file exists, we include it  
    //otherwise we throw and exception
           else {throw new Exception('<p>&nbsp;</p><p>&nbsp;</p><div class="loading_langfile_fails"><u>' .$_GET[LN].'.php </u> cannot be found</div>');} 
      }  
      catch(Exception $e){  
           echo $e->getMessage(); //which we catch and echo here  
           exit(0);  
      }  
This snippet is pretty much self explanatory. If the language file we are trying to include exists, we do include it, otherwise we throw and exception containing whatever error message we need to display.

And then we pull our current theme from the database
 //we first define our query  
      $q_tpl = "SELECT id_frontend_settings, frontend_settings_name, frontend_settings_value   
                FROM frontend_settings   
                WHERE frontend_settings_name = 'theme'  
           ";  
      MySQLModel::get_mysql_instance()->executeQuery($q_tpl); //we call on our MySQLModel to execute this query  
      if($myrows = MySQLModel::get_mysql_instance()->getRows($q_tpl)){ //if a result is found  
           define("CURRENT_THEME",$myrows['frontend_settings_value']); //we define a constant named CURRENT_THEME and we assign it the value the extract from the returned result  
      }  
We simply store our freshly-pulled theme name in the constant named CURRENT_THEME, which we will use throughout the CMS wherever necessary.

And now time to take care off the user wanting to log out
 $auth = AuthModel::getInstance(); we instantiate our Singleton authentification class  
      if(isset($_SESSION['c_login']) && isset($_SESSION['c_pwd'])){ if the user is logged in and wanting to log out, we invoke our let_out() method from the Authmodel class  
           if(isset($_GET[LETOUT]) && $_GET[LETOUT] == 'yes'){  
                $auth->let_out($_SESSION['c_login'],$_SESSION['c_pwd']);  
           }  
           if(preg_match('/\/out/',$_SERVER['REQUEST_URI'])){  
                $auth->let_out($_SESSION['c_login'],$_SESSION['c_pwd']);  
           }  
      }  
We store an instance of the AuthModel class in the variable named $auth. Then if the user is logged in and wanting to log out, we call the let_out() method, which will perform the logging out itself.



Then comes somehow the most important call of our index file, the call to the router's route() method.

This is exactly where we go deeper in trying to understand the core mechanisms that will make a module load upon being requested via the URL, be it a module to handle the serving of content, a module to allow the user to sign in or a module to manage some options in the back-end, it all loads from one object only - the router object.

In fact, back to design patterns, the very pattern that we will encounter now is called the factory pattern.

The factory pattern maybe be defined as a pattern in which the factory object creates other objects. This has a number of obvious benefits, such as the centralizing of the process of creating new objects. Therefore, if you need to change anything to the way you create and instantiate your new objects, then you only have to do so inside the factory.

In our case, the factory is our router. Our router, based on automatic URL parsing, will programmatically instantiate the controller of the module the client/user has asked for, and set it to work it by calling its initialize() method.

Just like with a regular factory. The client company needs a product, sends an order form, the order being our URL, the marketing people take the order form and tear down only the relevant page where the needed product is described, and handle it to the main plant people which in turn create the product and initialize it.

The __construct() method of our router class would be the order handling department, and the route method() would be the product building plant, in which is passed, as a parameter, the registry object we have created earlier, to be passed on, as a parameter again, to the controllers while being instantiated.

The registry works much as a stocking unit or a phone directory if you prefer, in the sense that you can store objects in it, to be later pulled out whenever required. How is that done? by using magic set() and get() methods, that will be triggered internally every time we will try to write/read to/from inaccessible properties, instead of having PHP generate an error.
 <?php  
      // application/Registry.php  
      class Registry{   

           private $vars = array(); //we declare the vars array as private  

           public function __set($index, $value){ we declare the magic __set() method, triggered when trying to write inaccessable properties  
                $this->vars[$index] = $value; // we store in the array the index associated with its value  
           }  

           public function __get($index){ we declare the magic __get() method, triggered when trying to access inaccessable properties  
                return $this->vars[$index]; //we return the value of a given index contained in the array  
           }  
      }  
      ?>  
In our __set() method, which is run when writing data to inaccessible properties and which accepts an index/value pair, we simply store the index in the private array named $vars, which is the stocking unit or phone directory really, and we store its value altogether.

In the __get() method, which is run when accessing data to from inaccessible properties, we simply return the value of a given array vars index by passing it as a parameter.

But back to our Router class
 <?php  
 class Router{
  
      private $path, $controller, $action;  //declaring the private properties we need

      public function __construct(){  //initializing our router object for when instantiated

           $exp_menu_type_list = explode(";", MENU_TYPE_LIST);  //we fetch and explode our menu type list again, we will need it here too

           if(isset($_GET[RLINK])){  //if a module name is set in the URL

                if(     CLEAN_URLS == true   // if clean URLs are on and google search is active and in two-column view mode
                     && GOOGLE_CSE_PAGING_TYPE == 'two_column'        
                     && preg_match('/cx=(.*)&gs=(.*)&sa.x=(.*)&sa.y=(.*)/', $_SERVER['REQUEST_URI']))  
                {$request = 'search';}  // we set our current module name as search, wich we store in the $request variable
                else{  //otherwise
   // if the set module name can be traced in our existing module type list
                     if(in_array($_GET[RLINK], $exp_menu_type_list)){$request = $_GET[RLINK];}  // we assign that name to $request
                     else{ 
                          // otherwise we use the DEFAULT_RLINK constant value  
                          $request = DEFAULT_RLINK;  
                     }  
                }       
           }  
           else{$request = DEFAULT_RLINK;} //// otherwise we use the DEFAULT_RLINK constant value 

           $this->controller = !empty($request) ? ucfirst($request) : 'Index';  // if $request is not empty, we assign its value (with the first letter made uppercase) to the controller property, otherwise, we assign it the value 'Index'
           $this->action = 'initialize';  // we set the action property to 'initialize'
      }  

      public function route($registry){ // our factory's building plant  

                require_once('application/BaseController.php'); we include our abstract controller class from which every created controller will extend  

                $file = 'application/controllers/' . $this->controller . 'Controller.php'; // we build the full controller's class path using the retrieve URL module variable retrieved in the __construct() method  

                if(is_readable($file)){ // if the controller class is readable,  
                     include $file; //we do include it  
                     $class = $this->controller . 'Controller'; //and set the $class variable  
                }  
                else{  
                     include 'application/controllers/Error404Controller.php'; //otherwise we include the 404error class  
                     $class = 'Error404Controller'; //and set the $class to that  
                }  

                $controller = new $class($registry); //whatever the included controller, we instantiate it, creating the $controller object, passing our $registry object as parameter, itself containing our template object.  

                if (is_callable(array($controller, $this->action))){ //if within the newly created $controller object there's an initialize() method (demanded by the baseController) that's callable   
                     $action = $this->action;  
                }  

                $controller->$action(); we do invoke that initialize() method we have called above 
      }  
 }  
We first need to pay attention to its __constructor() method, in which we retrieve our URL module variable ($_GET[RLINK]) (the page we tore down from the order form), assign it, after checking it corresponds to a valid module name, to the $controller property, otherwise we assign it the value 'Index'. If google CSE is active in two-column view mode and with clean URLs on, we assign 'search' to the $controller property.

Now onto the route() method, we bring our last core class, the base class, by including an abstract baseController class inside the router. The soon-to-be instantiated controller class will extend that BaseController class, so that it will inherit from its protected registry property, however, we as programmers will be free to decide how we want to implement the initialize() method that baseController is imposing on us. Note that as an abstract class, BaseController cannot be instantiated.
 <?php  
      // application/BaseController.php  
      abstract class BaseController{  
           protected $registry; //holds a protected reference of the registry  
           function __construct($registry){  
                $this->registry = $registry; //sets the registry  
           }  
           abstract function initialize(); //forces each sub-class to have an initialize() method   
      }  
      ?>  
But back to the route() method, if the router then finds our controller file to be readable, it includes it, otherwise it includes the Error404Controller class.

Whatever it has included, it instantiates. So now we have a controller object ready.

Further on, if that controller object's initialize() method is found callable, our router will call it. The initialize() method is where we will define the actions we want each controller to execute for itself and have it write some properties to the registry's template object to be passed directly to the corresponding view, in order to be used there. Within that method, we will also have each controller call the assign_theme() method of the registry's template object it holds.

This is how our user input is being routed. But then, we have to find and instantiate the model that corresponds to each created controller too. Because the models are the essential component that will provide our application with the business logic and data it needs. So, this is where the magic __autoload() function comes handy
 function __autoload($class_name){   
           //The $class_name parameter is internal to PHP, so we don't have to set it ourselves.   
           //In fact, this will be set for us when the controller will first call the model it needs but the model will still not be included though.   
           //That will trigger the __autoload() function, in which we specify where to look to find the needed model and include it.  
           try{  
                $filename = $class_name.'.php';  
                $file = 'application/models/' . $filename; //but we make sure the __autoload() function performs its search within our model directory only  
                if (file_exists($file)){include ($file);} // if the class can be found, where we have told the __autoload() function to look for, it includes it  
                else { //otherswise throws and expception  
                          throw new Exception('<p>&nbsp;</p><p>&nbsp;</p><div class="loading_model_fails">'.$GLOBALS['t_the_model_named'].' <u>' . $class_name . '</u> '.$GLOBALS['t_couldnt_be_found'].'</div>');   
                          // note the use of globals for handling the translations, we will come back to that in the next settlements  
                }  
           }  
           catch(Exception $e){  
                echo $e->getMessage();  
                exit(0);  
           }  
      }  
Did I say magic? Yes, it is a magic function, just as __set() and __get() are too. It will be called each time a called model class will not be recognized. So, it really is magic because, we do not know in advance which controller is going to be triggered. So, instead of having to include all our model classes, we only need to let PHP do the work of 'not recognizing' the model class the then-working controller object is trying to instantiate, call and instantiate it for us. Awesome, isn't it?

But what of the view then? As we said earlier, the view files, which are in fact basic php files acting as templates, will be included by the template object's assign_theme() method, itself called in the controller's initialize() method.
 <?php  
      // application/Template.php  
      class Template{  
           private $vars = array(); //declaring the array that will store the variables to be passed to the view  
           public function __set($index, $value){  
                     //magically assigning index/pairs to that array  
               $this->vars[$index] = $value;   
            }  
            public function assign_theme(){   
                //the method the controller calls once it has finished   
                //interacting with the model and wants to display the view  
                try{  
                          //We loop thru the vars array containing the variables the __set() method above has stored  
                                    foreach ($this->vars as $key => $value){  
                                         $$key = $value;  
                                         //we turn each key name to a variable using the $$ syntax, assigning it the same value it already held before entering the loop.  
                                    }  
                          $common_template_files = array('header','main','footer'); // in reality, we do not display only one php view file per/module,   
                          //as for obvious abstraction or visual design reasons, we choose to arbitrarily include the header, main and footer php view file,   
                          //and we will include [module_name].php view file only from within the main view file,   
                          //if the module_name variable has been sent by the controller  
                          foreach($common_template_files as $ctf){ //for each common tpl files  
                               $file = PATH_TO_THEMES.'/'.CURRENT_THEME.'/'.$ctf.'.php';  
                               //we build its full path  
                               if (!file_exists($file)){ //if the common tpl file does not exist  
                                    throw new Exception('<p>&nbsp;</p><p>&nbsp;</p><div class="loading_tpl_fails"><u>'.$ctf.'</u> '.$GLOBALS['t_couldnt_be_found'].'</div>');  
                                    // we throw an exception  
                               }       
                               else{  
                                    include($file); //but if it does exist, we include it  
                               }  
                          }  
                     }  
                     catch(Exception $e){  
                          echo $e->getMessage();  
                          exit(0);  
                     }  
                }  
           }  
      ?>  
Note from the code above that we don't actually display only one php view file per/module, as for obvious abstraction or visual design reasons, we choose to arbitrarily include the header, main and footer view file instead and we will include the [module_name].php view file only from within the main view file called main.php, if the module_name variable has been set and sent by the controller.

So here, in essence, we already have a full-blown MVC application in our hands. A router (factory) that loads the needed controller based on user input, a magically loaded model to provide, through its own set of properties and methods, for all the logic and data our application requires and a view file, fed by the template engine, itself containing a magic set() method and being contained in the registry object that was passed on to the controller as a parameter while being instantiated and that includes, at the controller's request, all the views needed for display, plus, that also stores, by means of set() methods, and displays all the data the controller wishes to handle to the view.

In the next settlement, we will set the focus on the controller, the model and the view, on how they are built, work together and we will come full circle by creating our first complete module, the page module. Do not miss it!

This article first appeared on Tuesday the 28th of October 2014 on RolandC.net.