How to build your own Multilingual PHP MVC CMS from scratch - Part 6 - Language/Theme Handling

Tuesday, February 16, 2016

Welcome to the sixth settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which I will get into full detail as to how themes and languages are managed in our project.

During the last settlement, I have demonstrated a way of building a user interface that allows us to register, sign in and review/update personal data. I have also challenged you, aided by all the knowledge you have gained so far, to try and build some additional features  like forgotten password, password changing and extra settings modules on your own project, operation that you should not be so difficult by now and that should considerably enhance the interface you have started to build, if you have.

But today, I will be getting in a totally different aspect of the project, gathering all the previously discussed elements about theme and language handling and coming full circle by giving you a complete, synthesized overview of the matter. I will point out however, that this will not have any direct relation with the back-end language and theme management features we will be starting to create during the next settlements, as this will in fact only be a discussion about everything pertaining to theme/language handling, mostly in front end.

Language handling


So, starting with languages and starting with the obvious, all language files, whether used in back-end or in front-end, will fall under the same directory structure as shown right bellow




The file named encoding.php merely holds an array containing charsets on a per language basis, we will later review the method it is being useful to.
 <?php  
   
      /* languages/encoding.php */  
   
      $encod = array(  
           'en' => 'utf-8',  
           'fr' => 'utf-8',  
           'uk' => 'utf-8',  
           'ru' => 'utf-8',  
           'de' => 'utf-8',  
           'nl' => 'utf-8',  
           'se' => 'utf-8',  
           'es' => 'utf-8'  
      );       
 ?>  

The file named translate.js contains a script that's needed to help localize all Javascript strings. Since I have already presented it in a previous article (http://www.sitepoint.com/localizing-javascript-strings-php-mvc-framework/), I will not need to do so here. 

Obvious is the fact that each language folder contains its own flag, in .png format.

One thing that differentiates the front-end section however, is that a template folder has been added to address template needs while sending out emails, eg: confirmation of registration, contact form and new password sending. Indeed, a user who has registered using Swedish may not like to receive a confirmation all written in English or in any language other than Swedish for that matter. 




Let's now take the example of registration_en.php
 <?php   
   
 /* languages/templates/en/registration_en.php */  
   
 $content = "  
 <html>  
 <head>  
 <title>Your registration</title>  
 </head>  
 <body>  
 <p>Thank you for your registration on \"".SITE_TITLE."\"!</p>  
 Your credentials are  
 <table>  
 <tr>  
 <td>Login : </td>  
 <th>".$login."</th>  
 </tr>  
 <tr>  
 <td>Pass : </td>  
 <th>".$password."</th>  
 </tr>  
 </table>  
 <p>  
 Regards,<br />   
 the site's team  
 </body>  
 </html>  
 ";  
 ?>  
Knowing HTML, you can all imagine what the output is going to look like in your favorite email client program. 

Interesting too is the constant named CONTACT_FORM_RECEIVER_LANGUAGE, which is used to define the language in which the administrator will like to receive its feedback emails.
 define("CONTACT_FORM_RECEIVER_LANGUAGE","en"); // the code of the language in which you wish to receive the user contact messages  
Now for the core language files that hold most of the PHP localized strings, let's take the example of en.php
 <?php  
   
 /* from languages/en/en.php */  
   
 $t_welcome = 'Welcome';  
 $t_contact = 'Contact';  
 $t_registration = 'Register';  
 $t_reg = 'Register';  
 $t_copyright = 'Copyright';  
 $t_submit = 'Submit';       
 $t_save = 'Save';  
 ...  
 ?>  
As an unspoken rule, localized PHP strings follow the format $t_my_word_or_sentence = 'My word or sentence'; Thus the variable name is always lowercase, each word separated by an underscore, and t_ is appended at the beginning of each term. HTML tags may be used inside the literal string part, constants too, but best is to avoid this whenever possible, for readability at least.

And now, time to get into the controversial debate about global variables. Since day 1, I have chosen to use global variables to access the translation strings we have just discussed. So, for example, throughout the whole site, we may access $t_welcome by using $GLOBALS['t_welcome'], which turns out to be greatly convenient, for a number of reasons.

Although... I can hear some of you screaming 'globals? DO NOT USE GLOBALS!', which is sometimes fair in its own right. But passed the fact that I myself am totally not convinced using globals for my translation system here will affect the project or anyone instantiating/using it, fact is, I have even yet to be convinced by the generally advised methods in the field, which are either about stacking everything into array from which translations are latter picked, or using POEdit/gettext which I don't feel truly answers the specific needs I have been having for this project so far. 

So that, the array method being pretty cumbersome to maintain and great only for small localization projects and also, with over 1000 translation strings for each language, unless someone comes up with a great new way to handle localization that's easily and quickly implementable, even in large-scale environments, that's the method I will keep using. Besides, not only this is the one method I am myself being the most comfortable with, this is also a method that neither ever failed me nor made waves somewhere else in the project since I have started implementing it. So, as programmers too sometimes say: 'Don't repair it if it ain't broken!'.

But the truth is, within our project, there is much more to language handling than mere string localization. 

Let's start from the localization table and then go on with the language selector bar 
 <?php       
 --  
 -- Table structure for table `localization`  
 --  
   
 CREATE TABLE `localization` (  
  `id_localization` smallint(6) NOT NULL,  
  `localization_name` varchar(3) NOT NULL,  
  `localization_flag` varchar(8) NOT NULL,  
  `localization_enabled` enum('no','yes') NOT NULL,  
  `localization_default` enum('no','yes') NOT NULL,  
  `localization_timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP  
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;  
   
 --  
 -- Dumping data for table `localization`  
 --  
   
 INSERT INTO `localization` (`id_localization`, `localization_name`, `localization_flag`, `localization_enabled`, `localization_default`, `localization_timestamp`) VALUES  
 (7, 'se', 'se.png', 'no', 'no', '2014-10-24 19:17:26'),  
 (8, 'es', 'es.png', 'no', 'no', '2014-10-24 19:17:26'),  
 (5, 'ru', 'ru.png', 'no', 'no', '2014-10-24 19:17:26'),  
 (6, 'nl', 'nl.png', 'no', 'no', '2014-10-24 19:17:26'),  
 (3, 'fr', 'fr.png', 'yes', 'yes', '2014-10-24 19:17:26'),  
 (4, 'uk', 'uk.png', 'no', 'no', '2014-10-24 19:17:26'),  
 (1, 'en', 'en.png', 'yes', 'no', '2014-10-24 19:17:26'),  
 (2, 'de', 'de.png', 'no', 'no', '2014-10-24 19:17:26'),  
 (10, 'dk', 'dk.png', 'no', 'no', '2014-10-24 19:17:26');  
   
 --  
 -- Indexes for dumped tables  
 --  
   
 --  
 -- Indexes for table `localization`  
 --  
 ALTER TABLE `localization`  
 ADD PRIMARY KEY (`id_localization`);  
   
 --  
 -- AUTO_INCREMENT for dumped tables  
 --  
   
 --  
 -- AUTO_INCREMENT for table `localization`  
 --  
 ALTER TABLE `localization`  
 MODIFY `id_localization` smallint(6) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;  
 ?>  
Nothing too unexplainable about this one, although it's a good occasion to remind you the timestamp field is here only to help force MySQL perform an update of at least one row every time we perform an operation but the data hasn't changed.

And from this table, we can start building a language selector bar users will be able to interact with, shifting from one language to another easily.
 <?php  
      /* from themes/your_style/main.php */  
   
      // we fetch an instance of the localization model   
      // and call the method that will retrieve all enabled languages  
      $lang_bar = LocModel::getInstance()->getData_language_bar();  
   
      foreach($lang_bar as $ln_item){//for each language  
   
           if(GRAPHICAL_LN_BAR == true){   
                // if flag view is set, we display the flag for each language  
                // wrapped in the target url  
                // The get_ln_straight() method from bellow  
                // will take in a URL and process it to match the language   
                // on which the mouse is hovering, 
                // all while preserving the other elements the current URL contains, as well as their order  
           ?>   
                <a href="<?php echo LocModel::getInstance()->get_ln_straight($ln_item[0]); ?>"><img src="<?php echo CLEAN_PATH.'/'; ?>languages/<?php echo $ln_item[0]; ?>/<?php echo $ln_item[1]; ?>" width="<?php echo FLAG_WIDTH; ?>" height="<?php echo FLAG_HEIGHT; ?>" alt="<?php echo isset($GLOBALS['t_'.$ln_item[0].'_lng'])?$GLOBALS['t_'.$ln_item[0].'_lng']:"$ln_item[0]"; ?>" /></a> <?php   
           }  
           else{   
           ?>  
                <a href="<?php echo LocModel::getInstance()->get_ln_straight($ln_item[0]); ?>"><u><?php echo $ln_item[0]; ?></u></a> <?php   
           }  
      }  
 }  
 ?>  
There is not much to write about the commented above code, expect for the call to the get_ln_straight() method, which will, no matter what the current URL is, make sure the language that's being displayed in the target link matches the flag/language name on which the mouse of the user is hovering, all while preserving the other elements the current URL contains, as well as their order. But before that, let's consider the useful getData_language_bar() method
 <?php  
   
      /* from application/models/LocModel.php */  
        
      ...  
      public function getData_language_bar(){  
   
           $this->locData = array();  
           $line = array();  
   
           // we first check what language directories (inside both front-end and back-end /language dirs)   
           //exist physically on server. We use PHP's glob() function for that  
           $language_directories = glob(PATH_TO_LANGUAGES . '/*' , GLOB_ONLYDIR);  
           $language_admin_directories = glob('admin/'.PATH_TO_LANGUAGES. '/*' , GLOB_ONLYDIR);  
   
           $admin_language_list = array();  
   
           foreach($language_admin_directories as $admin_dir){  
                $exp_admin_dir = explode("/",$admin_dir);  
                array_push($admin_language_list, end($exp_admin_dir));  
           }  
   
           $language_list = array();  
   
           foreach($language_directories as $dir){  
   
                $exp_dir = explode("/",$dir);  
   
                // if the currently processed front-end language dir can be found in the admin language dir array,  
                // we push it inside the $language_list array, to be used later for checking   
                if( in_array( end($exp_dir), $admin_language_list)){array_push($language_list, end($exp_dir));}  
           }  
   
           //then we pull out the language list from the db  
           $q_ln_bar = "SELECT id_localization, localization_name, localization_flag, localization_enabled   
                           FROM localization   
                           WHERE localization_enabled = 'yes'  
                           ORDER BY id_localization ASC                      
                          ";  
   
           MySQLModel::get_mysql_instance()->executeQuery($q_ln_bar);  
   
           while($mylnbar = MySQLModel::get_mysql_instance()->getRows($q_ln_bar)){  
        
                //we now compare the each freshly pulled language row from the DB   
                //with our array of check existing physical language dirs  
                if(in_array($mylnbar['localization_name'], $language_list)){   
                     // so that, basically, if the name of the currently processed language corresponds to existing language folders   
                     //in both back-end and front-end at the same time, we consider that language valid and stack it in an array,   
                     //that will be stacked with other arrays in one more array that will be returned to us as a whole   
                     array_push($line, $mylnbar['localization_name'],$mylnbar['localization_flag']);  
                     array_push($this->locData,$line);$line = array();  
                }  
           }  
           return $this->locData;   
      }       
      ...  
   
 ?>  
And then the get_ln_straight() method we have started to discuss earlier, here to ensure the user will always fall back on its feet while changing the language from any page
 <?php  
      /* from application/models/LocModel.php */  
        
      ...  
      public function get_ln_straight($lang){  
        
           //using $_SERVER['QUERY_STRING'] will make our life a lot easier when it comes to dealing with clean URLs  
        
           if(empty($_SERVER['QUERY_STRING'])){ // if $_SERVER['QUERY_STRING'] turns out empty we use default values  
                if(DEFAULT_PLINK){$plink = '&'.PLINK.'='.CURRENT_PLINK;} else{$plink = '';}  
                $_SERVER['QUERY_STRING'] = RLINK.'='.CURRENT_RLINK.$plink.'&'.LN.'='.CURRENT_LANG;  
           }   
   
           $this->query_string = $_SERVER['QUERY_STRING'];  
           $pat = '/'.LN.'=[a-z]{2}/';  
           $pat2 = '/language=[a-z]{2}/'; //that's for Google CSE  
   
           //if we can spot a valid URL language atom, we replace it by the then-sought language, $lang,   
           //initially passed as parameter to the method  
           if(preg_match($pat, $this->query_string, $matches, PREG_OFFSET_CAPTURE, 3)){  
                $rpl = LN.'='.$lang;  
                $this->query_string = preg_replace($pat, $rpl, $this->query_string);  
           }  
   
           if(preg_match($pat2, $this->query_string, $matches, PREG_OFFSET_CAPTURE, 3)){  
                $rpl2 = 'language='.$lang;  
                $this->query_string = preg_replace($pat2, $rpl2, $this->query_string);  
           }  
   
           //normally at that point, we should already be fine,  
           //but clean urls come complicating everything again...  
           if(CLEAN_URLS == true){//if clean urls are on,  
                  
                //we don't just preg_replace our query string, we explode it.  
                $exp_query_string = explode('&', $this->query_string);  
   
                $markers = array(  
                     //and here come markers  
                     //basically, markers are something we use with clean_urls whenever thought necessary,   
                     //to avoid collision between .htaccess rewrite rules, when there are (too) many of them  
                     'pn' => PGN,  
                     //note that 'pn' is what will be written in .htaccess  
                     //and PGN is the constant name that normally holds the page number url variable 'pgn'.   
                     // again, with clean urls being off, you could assign any other value than pgn to the constant PGN  
                     'sn' => SN  
                );  
   
                $fake_string = array(); //this will hold our new url string until imploded  
   
                foreach($exp_query_string as $atom){ //for each atom of the old string (x=y)  
   
                     $exp_atom = explode("=",$atom); // we explode it (the atom)  
   
                     $needle = $exp_atom[0];     //and take its left part, called the needle in this case  
   
                     if(!preg_match('/success/',$atom)){// we avoid the word success in our algorithm for collision reasons       
                            
                          //we then search for that needle inside the haystack (marker's array)  
                            
                          if(in_array($needle, $markers) ){   
                               //if found, we will create a new clean atom with the marker in it.   
                               //because that's what markers are here for - making clear what the nature of the value they bare is  
   
                               $key = array_search($needle, $markers); //so we get its key   
   
                               $new_atom = $key.'/'.$exp_atom[1]; //and actually create the new 'clean' atom using that key,   
                               //a slash sign and the value of the old one  
                          }  
                          else{$new_atom = $exp_atom[1];} // otherwise, if the atom does not need a marker, we retain only the x part of the old string  
   
                          array_push($fake_string,$new_atom); //and we stack each resulting atom in our $fake_string array  
                     }  
                }  
   
                ///and then we turn that array to a string, using '/' to implode all parts  
        
                $new_string = implode("/", $fake_string);  
                  
                //we quickly append our clean path constant and we're good to go  
                $this->query_string = CLEAN_PATH.'/'.$new_string;  
   
                //not just yet though... in case of Google CSE  
                if(preg_match('/cx=(.*)&gs=(.*)&sa.x=(.*)&sa.y=(.*)/', $_SERVER['REQUEST_URI'])){   
                     // so, if google search is on  
                     //we explode not the query string but the request URI this time  
                     $exp_q_s = explode('?',$_SERVER['REQUEST_URI']);  
                     $this->query_string .= '?'.$exp_q_s[1]; //and retain only the parts proper to Google,   
                     //(so the user keeps seeing its search results after it has selected another language),   
                     //we concatenate them to the query string we've just built and now we're good to go.  
                }  
           }   
           else{  
                //else, if clean urls weren't set, we only need to append $_SERVER['PHP_SELF']   
                //instead of the clean path constant before the new string we have built   
                $this->query_string = $_SERVER['PHP_SELF'].'?'.$this->query_string;  
           }  
   
           return $this->query_string; //we return our resulting string  
      }  
 ?>  
Then still from the localization model, the get_encoding_straight() method, which if passed the current language as parameter, will return the corresponding charset, as defined in languages/encoding.php. This method will be used in header.php, both in front-end and back-end. By the way, we will get to the header files today while getting in the subject of theme handling, so we will have a chance to see the get_encoding_straight() method in action there.  
 <?php  
      /* from application/models/LocModel.php */  
   
      ...  
      public function get_encoding_straight($lang){  
   
           require PATH_TO_LANGUAGES.'/encoding.php'; // we require the file that contains the $encod array, as discussed earlier today.  
   
           if (array_key_exists($lang,$encod)){ //if we can find the current language in that array, as key  
                $this->encoding = $encod[$lang]; we then use its corresponding value  
           }  
           else{$this->encoding = 'utf-8';} //else we default to utf-8  
   
           return $this->encoding; //we return what we have then  
      }  
 ?>  
Finally, a word about the choice of character sets though: while this may feel odd to some of you that I sometimes modify the MySQL charset to Latin1 before attempting a query, from the way the database tables for this project were originally set up (latin1_swedish_ci), plus using utf-8 as HTML charset on all pages, no encoding-related display issues have yet arisen while testing the system against languages such as French, Russian or even Swedish, this on both Windows/*nix platforms. In other words, why changing what already works? But still, if you somehow end up getting encoding issues that I myself do not get, just try other values to see what works for you, although, everyone should be just fine with the settings as they currently are, regardless of the (modern) browser it's being displayed in.

Theme handling


At the beginning of these series, I have presented the template engine that dynamically includes all the necessary view files, plus takes care off the passing of variables from the controller. But there's of course more to theme handling than this powerful routine. I justify:

Let's start by analyzing the overall structure of the theme folder




Then the same structure in more detail

 

From this picture above, you can see there are two different styles being operated, which are named skane and sparta. These folders exist in the back-end section too but vary in content.

At root (/themes/your_style), you can find all the view files proper to each module and not only. There lie our common template files header.php, main.php and footer.php and we can also find secondary view files like vf_image_u.php or follow.php, which respectively create an image for the captcha in registration.php and create a social media follow bar to be included wherever found stylish inside main.php. More interesting view files can be found there, like paging.php but we will get to them during the forthcoming settlements only.

Jumping straight into the css folder, we find name_of_the_style.css.php which is the main css file for any given theme. It bares a .php extension because on some occasions we need to use PHP variables there. We also find specific CSS files for the libraries we use like dTree, which has dtree.css sitting there and so on.

On the same level as the css folder, we find a folder called images. That folder holds two sub-folders called favicon and servicing. While it looks obvious what the favicon folder is here for, the servicing folder however requires a little explanation. It is a folder that holds images that will not only build on the visual identity of a given theme, but that are also dedicated to common navigation items. As an example, servicing is the folder where you will find small arrows, warning and missing page/module icons. And since menus are considered standard navigation items, you will also find two sub-folders called dtree and hmenu, which contain images for use with the menu system you already know of. In fact a third sub-folder to the servicing folder can even be found, called rss, which contains a few rss feed logo .png format images to choose from (see RSS_ICON_FILE in config.php).

At the root level of /themes/your_style/images, therefore at the same level where the favicon and servicing folder are, can main logo images be found.

Entering the /themes/your_style/css folder again, we find there's an image folder too, not to be confused with /themes/your_style/images. This will contain images for use in Javascript dialog boxes. Note these dialog boxes are also configurable via /themes/your_style/css/dialog_box.css.
 
Back at the root level of the themes folder reside two of our common template files. The first one is header.php
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
 <?php  
 // here we sort out language code names to be compatible with most standards  
 if(CURRENT_LANG){  
      if(CURRENT_LANG == 'uk'){$tinymce_lang = 'uk_UA';}  
      elseif(CURRENT_LANG == 'fr'){$tinymce_lang = 'fr_FR';}  
      elseif(CURRENT_LANG == 'se'){$tinymce_lang = 'sv_SE';}  
      else{$tinymce_lang = CURRENT_LANG;}  
 }  
 else{$tinymce_lang = 'en';} // if nothing was found, we default to English  
 ?>  
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $tinymce_lang; ?>" lang="<?php echo $tinymce_lang; ?>">  
 <head>  
 <?php   
 $header_utils = new UtilsModel();  
 $met = $header_utils->get_meta_info(); //we fetch meta infos from the DB, this is being discussed further down the page.  
   
 foreach($met as $m){ //for each meta entry...   
      if($m[0] == 'title'){ // if tag is title  
           echo '<title> ';   
           if(isset($page_name)){echo $page_name;}   
           else{  
                if(isset($module_name)){  
                     echo isset($GLOBALS['t_'.strtolower($module_name)]) ? $GLOBALS['t_'.strtolower($module_name)] : $module_name;  
                }  
           }  
           echo '</title>';   
           }  
      else{ //if not title tag, we use the rest of what we have pulled from the meta table.  
           if($m[0] == 'http-equiv'){   
                if($m[1] == "Content_Type"){$m[1] = "Content-Type";}   
                $char = " charset = ".LocModel::getInstance()->get_encoding_straight(CURRENT_LANG)."";  
                $m[2] .= $char;  
           }  
           echo '<meta '.$m[0].'="'.$m[1].'" content="'.$m[2].'" />';  
      }  
 }  
 ?>  
 <!-- now we link all the CSS/JS we need on our project -->  
 <link rel="shortcut icon" href="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES; ?>/<?php echo CURRENT_THEME; ?>/images/favicon/favicon.ico" type="image/x-icon" />  
 <link rel="stylesheet" href="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES; ?>/<?php echo CURRENT_THEME; ?>/css/<?php echo CURRENT_THEME; ?>.css.php" type="text/css" />  
 <link rel="stylesheet" type="text/css" href="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES; ?>/<?php echo CURRENT_THEME; ?>/css/dtree.css" />  
 <link rel="stylesheet" type="text/css" href="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES; ?>/<?php echo CURRENT_THEME; ?>/css/hmenu.css" />  
 <link rel="stylesheet" type="text/css" href="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES; ?>/<?php echo CURRENT_THEME; ?>/css/dialog_box.css" />  
 <link rel="stylesheet" type="text/css" href="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES; ?>/<?php echo CURRENT_THEME; ?>/css/dtp.css" />  
 <script src="<?php echo CLEAN_PATH.'/'; ?>languages/<?php echo CURRENT_LANG; ?>/JS/<?php echo CURRENT_LANG; ?>.js" type="text/javascript"></script>  
 <script src="<?php echo CLEAN_PATH.'/'; ?>languages/translate.js" type="text/javascript"></script>  
 <?php  
 include 'public/js/main.php';  
 include 'public/js/dtree.php';  
 ?>  
 <script src="<?php echo CLEAN_PATH.'/'; ?>/public/js/hmenu.js" type="text/javascript"></script>  
 <?php include 'public/js/dialog_box.php'; ?>  
 <script src="<?php echo CLEAN_PATH.'/'; ?>public/js/dtp.js"type="text/javascript"></script>  
 <!-- and in case Javascript is disabled, we use a noscript tag -->  
 <noscript><div class="disabled_feature"><?php echo $GLOBALS['t_javascript_has_been_disabled']; ?> <?php echo $GLOBALS['t_please_reenable_it']; ?> - <a href="http://www.enable-javascript.com" target="_blank"><u><?php echo $GLOBALS['t_instructions_here']; ?></u></a></div>  
 </noscript>  
 </head>  
So, in header.php, we first sort out our language code names, for enhanced compatibility with the most standards officially in use and we use the freshly defined code in our html xml declaration. Then we pull all meta info from the DB table called meta, that looks as follows
 --  
 -- Table structure for table `meta`  
 --  
   
 CREATE TABLE `meta` (  
  `id_meta` tinyint(4) NOT NULL,  
  `meta_type` varchar(12) NOT NULL,  
  `meta_name` varchar(40) NOT NULL,  
  `meta_value` text NOT NULL  
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;  
   
 --  
 -- Dumping data for table `meta`  
 --  
   
 INSERT INTO `meta` (`id_meta`, `meta_type`, `meta_name`, `meta_value`) VALUES  
 (1, 'title', 'title', 'Gumbo-CMS'),  
 (2, 'name', 'description', 'Gumbo (or Gumbo-CMS) is a Mutilingual MVC CMS written \'from scratch\' in PHP 5 and that requires Apache 2.2+ and MySQL 5.6+'),  
 (3, 'name', 'keywords', 'cms, mvc, php, multilingual cms'),  
 (4, 'name', 'robots', 'index, follow'),  
 (5, 'name', 'google_bot', 'index, follow'),  
 (6, 'name', 'google', 'notranslation'),  
 (7, 'http-equiv', 'Content_Type', 'text/html;');  
   
 --  
 -- Indexes for dumped tables  
 --  
   
 --  
 -- Indexes for table `meta`  
 --  
 ALTER TABLE `meta`  
  ADD PRIMARY KEY (`id_meta`);  
   
 --  
 -- AUTO_INCREMENT for dumped tables  
 --  
   
 --  
 -- AUTO_INCREMENT for table `meta`  
 --  
 ALTER TABLE `meta`  
  MODIFY `id_meta` tinyint(4) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8;  
and we start looping the results we have successfully fetched.

A note about meta tags though, please visit https://support.google.com/webmasters/answer/79812?hl=en for a brief overview of the tags Google Inc. understands.

Then we go onto including all needed CSS/Javascript files and end up using a noscript tag that will warn users in case Javascript is disabled on their viewing device. That's all there is to the header file found in the front-end. In a next settlement though, we will see the header file for the back-end contains a lot more information, primarily due to TinyMCE being initialized there. Note that header.php ends with the closing head tag. 

Meanwhile, in footer.php, the starting tag is a tr tag and the closing tag is the closing html tag
 <?php  
      /* themes/your_style/footer.php */  
 ?>  
      <tr class="footer_tr">  
           <td colspan="2" class="footer">  
                <?php echo $GLOBALS['t_copyright'].' '.COPYRIGHT; ?>  
           </td>  
      </tr>  
 </table>  
 </div>  
 </body>  
 </html>  
Nothing too difficult about this one as well, and that wraps up this settlement in which you have learned more about the way languages and themes are dealt with in this project, getting for now a complete overview, for at least the front-end part.

But next time, I will take these series to yet another level, getting onto the other side of the fence by introducing you to the back-end side of our CMS, starting with the building of a really exciting language management module. Creating this new module will allow me, among other things, to tell you more about the paging and sorting features many of the back-end modules actually depend on.

This article first appeared Thursday the 16th of February 2016 on RolandC.net.

How to build your own Multilingual PHP MVC CMS from scratch - Part 5 - Frontend (part 3): User Interface

Tuesday, December 29, 2015

Welcome to the fifth settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which we will be building an interface for the user to sign in and update its personal data.

During the last settlements, we have created a page module that allows the user to display a specific page based on URL input and aided by some third-party scripts, we have also created a customizable menu display system that comes in two layout flavors, vertical and horizontal. In a future settlement, I will show you how to create the back-end feature that will allow you to completely edit the menus stored in the database.

But for now, we will set our interest onto building a user interface that will allow for signing into the system as well as updating personal data. Now, since in reality, the user’s experience on this project encompasses at least 6 whole modules, which are: registration, sign in, forgotten password, account, profile, settings, password changing, plus the use of authentication model as well and that we obviously cannot review all of them at once, we will only be reviewing some of them, which are the registration, signin and profile modules. And this will still be a lot, so I will be focusing only on the parts that are of interest. This makes sense since there is no point in explaining how a controller works over and over again.

Anyway, the table named ‘users’ represents the natural entry point through which we will start digging our user interface.
 --  
 -- Table structure for table `users`  
 --  
   
 CREATE TABLE `users` (  
  `id_user` int(11) NOT NULL,  
  `user_login` varchar(12) NOT NULL,  
  `user_password` text NOT NULL,  
  `user_email` varchar(72) NOT NULL,  
  `user_country` varchar(255) NOT NULL DEFAULT 'uk',  
  `user_gender` enum('else','female','male') NOT NULL,  
  `user_birthdate` varchar(100) NOT NULL,  
  `user_type` enum('0','1','2') NOT NULL,  
  `user_ip` varchar(16) NOT NULL,  
  `user_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;  
   
 --  
 -- Dumping data for table `users`  
 --  
   
 INSERT INTO `users` (`id_user`, `user_login`, `user_password`, `user_email`, `user_country`, `user_gender`, `user_birthdate`, `user_type`, `user_ip`, `user_timestamp`) VALUES  
 (1, 'test', 'c08ac56ae1145566f2ce54cbbea35fa3', 'test@mail.com', 'fidji', 'male', '08/02/1985', '1', '127.0.0.1', '2015-10-02 00:55:41');  
   
 --  
 -- Indexes for dumped tables  
 --  
   
 --  
 -- Indexes for table `users`  
 --  
 ALTER TABLE `users`  
  ADD PRIMARY KEY (`id_user`);  
   
 --  
 -- AUTO_INCREMENT for dumped tables  
 --  
   
 --  
 -- AUTO_INCREMENT for table `users`  
 --  
 ALTER TABLE `users`  
  MODIFY `id_user` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;  
Relying on the users table is the table named user_settings, which I will expose while discussing the profile module. Note the user_type field will not be of much help since permission levels will not be implemented, at least not until the end of the series.

Without any transition whatsoever, let’s jump right in the registration form. There’s no point in providing a user with an interface to sign in if that user was never given the opportunity to register beforehand, right?
 /* themes/your_style/registration.php */  
   
 <div align="center">  
 <?php  
 // we first take care off displaying feedback, if it contains error messages  
 if(isset($feedback) && !is_string($feedback)){  
 ?>  
      <table border="0" cellpadding="2" cellspacing="2" align="center">  
      <ul>  
      <?php  
      foreach($feedback as $err){  
           foreach($err as $er){  
                $msgs = '';  
                $exp_msg = explode("#",$er[1]);  
                $cx = 0;  
                foreach($exp_msg as $em){  
                     if($cx < sizeof($exp_msg)-1){$trailer = '; ';}else{$trailer = '';}  
                     $msgs .= $GLOBALS['t_'.$em].$trailer;$cx++;  
                }  
                ?>  
                <tr><td><div class="response_negative" style="font-size:12px;"><li><?php echo ucfirst($GLOBALS['t_'.$er[0]]); ?> -> <?php echo $msgs; ?></div></li></td></tr>  
      <?php   
           }  
      }  
 ?>  
      </ul>  
      </table>  
      <br />  
      <?php  
 }  
 if(isset($feedback) && $feedback == 'failed'){ ?><div class="failed"><?php echo $GLOBALS['t_problem_registering_you']; ?></div><?php }  
   
 //now preparing for action urls to deploy  
 if(CLEAN_URLS == true){$url = CLEAN_PATH.'/'.CURRENT_RLINK.'/'.CURRENT_LANG;}  
 else{$url = $_SERVER['PHP_SELF'].'?'.RLINK.'='.CURRENT_RLINK.'&'.LN.'='.CURRENT_LANG;}  
   
 $utils = new UtilsModel();  
 $user_ip = $utils->get_user_ip();  
 ?>  
 <br /><br />  
 <p>&nbsp;</p>  
 </div>  
   
 <!-- the actual registration form -->  
 <form id="frm_registration" name="frm_registration" enctype="multipart/form-data" method="post" action="<?php echo $url; ?>" onsubmit="return Check_registration(this,'off');">  
       
 <table border="0" cellpadding="2" cellspacing="0" align="center">  
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_login']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="text" id="login" name="login" value="<?php echo isset($_POST['login'])?htmlspecialchars($_POST['login']):""; ?>" maxlength="<?php echo MAX_CHARS_LOGIN; ?>" onkeyup="check_login_exists();" /></td>  
           <!-- pay attention to the ternary statements - if post_login is set we use it,   
           //otherwise we leave the field empty - this allows for field rememberance,   
           //so that if for any reason the form gets reloaded, the user won't have to type everything over again -->  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_password']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="password" id="password" name="password" value="<?php echo isset($_POST['password'])?htmlspecialchars($_POST['password']):""; ?>" maxlength="<?php echo MAX_CHARS_PWD; ?>" /></td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_email']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="text" id="email" name="email" value="<?php echo isset($_POST['email']) ? htmlspecialchars($_POST['email']):""; ?>" maxlength="<?php echo EMAIL_MAX_LENGTH; ?>" onkeyup="check_email_exists();" /></td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_country']; ?>*</td>  
           <td align="left" class="input">  
           <select name="country">  
           <?php   
           require('lists/countries/countries.php'); //for clarity, we use a country list  
   
                if(isset($country_list) && !empty($country_list)){  
   
                foreach($country_list as $country){                      
                     if(isset($_POST['country']) && $_POST['country'] == $country[1]){$sel = ' selected=selected';}else{$sel = '';}  
                ?>  
                     <option value="<?php echo $country[1]; ?>"<?php echo $sel; ?>><?php echo $country[0]; ?></option>  
                     <?php  
                     }  
                }   
           ?>  
           </select>  
           </td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_gender']; ?>*</td>  
           <?php  
 // if(isset($_POST['gender'])){echo $_POST['gender'];$checked = ' checked';}else{$checked = '';}   
 ?>  
           <td align="left" class="input">  
                <table border="0" cellpadding="2" cellspacing="0" width="100%">  
                     <tr>  
                          <td><input type="radio" id="gender" name="gender" value="male" checked />&nbsp;&nbsp;<?php echo $GLOBALS['t_male']; ?></td>  
 </tr><tr>  
   
                     <td><input type="radio" id="gender" name="gender" value="female"<?php if(isset($_POST['gender']) && $_POST['gender'] == 'female'){echo ' checked';} ?> />&nbsp;&nbsp;<?php echo $GLOBALS['t_female']; ?></td>  
                     </tr>  
                </table>  
           </td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_birthdate']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="text" id="birthdate" name="birthdate" value="<?php echo isset($_POST['birthdate'])?htmlspecialchars($_POST['birthdate']):""; ?>" maxlength="10" onkeyup = "displayDatePicker('birthdate');" onclick = "displayDatePicker('birthdate');" /></td>  
      </tr>  
        
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_newsletter']; ?></td>  
           <td align="left" class="input"><input type="checkbox" name="newsletter" value=""<?php if(isset($_POST['newsletter'])){$checked = ' checked';}else{$checked = '';} echo $checked; ?> /></td>  
      </tr>  
   
      <tr align="right">  
           <td align="center" valign="middle" class="std" valign="bottom"><?php echo $GLOBALS['t_verification_code']; ?></td>  
           <td align="center">  
                <table border="0" cellpadding="0" cellspacing="0" width="100%">  
                     <tr align="center">  
                          <td valign="middle"><img src="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES.'/'.CURRENT_THEME.'/vf_image_u.php'; ?>" />&nbsp;</td>  
                          <td><input name="vercode_u" type="text" id="vercode_u" autocomplete="off" maxlength="5" style="height:;width:155px;font-size:px;background-color:#;color:#;" /></td>  
                     </tr>  
                </table>  
           </td>  
      </tr>  
   
      <tr>  
           <td><input type="hidden" name="type" value="1" /></td>  
           <td><input type="hidden" name="ip" value="<?php echo $user_ip; ?>" /></td>  
      </tr>  
   
      <tr>  
           <td class="std" colspan="2" align="center">  
           <br />  
           <input type="submit" id="submit" name="submit" class="submit" value="<?php echo $GLOBALS['t_submit']; ?>" /></td>  
      </tr>  
 </table>  
 </form>  
There really isn’t much to the above code. It starts off displaying feedback messages, if those are containing registration errors. Then the actual registration form begins.

Ternary syntax is used for field remembering. Eg:
 <input class="text" type="text" id="login" name="login" value="<?php echo isset($_POST['login'])?htmlspecialchars($_POST['login']):""; ?>" maxlength="<?php echo MAX_CHARS_LOGIN; ?>" />  
This tells that if a $post login is set, let’s use it, otherwise let’s leave empty.

Client-side Javascript validation is operated by a call to the Javascript Check_registration() function that can be found in public/js/main.php. In the event that the user disables Javascript, server-side validation is triggered via the check_form_errors() method as discussed further bellow.

During both onkeyup and onclick events, a dynamic Javascript/CSS-written date picker library is instantiated and appears below the birthdate field. This library will provided be as an attachment along with the corresponding CSS file. You may choose not to use this library and it will make no difference in terms of validation, as the birthdate field is validated client-side as well as server side. This third-party open-source date picker is here to operate only as an aesthetically pleasant date picker, enhancing the overall visual aspect.

A country list file and script is used for country selection.
 <?php  
   
 /* lists/countries/coutries.php */  
   
 require('array_countries.php');  
 $country_list = array();  
 $line = array();  
   
 foreach($array_country_list as $c){  
      array_push($line, $GLOBALS['t_'.strtolower($c)], strtolower($c));  
      array_push($country_list, $line);  
      $line = array();  
 }  
 asort($country_list);  
 ?>  
This snippet is pretty much self-explanatory and draws from the array bellow to create a new array containing the internationalized name of each country along with the all-lowercase formatted name of that same country.

Note the alphabetical sorting operated by the asort() function does not work too well with languages like Russian, or with html ascii codes because as of now, that’s as far as PHP’s support in that matter goes.
 <?php  
   
 /* lists/coutries/array_countries.php */  
   
 $array_country_list = array(  
 'Afghanistan',  
 'Albania',  
 'Algeria',  
  ...,  
 'Zimbabwe'  
 );  
 ?>  
The full array is available as attachment, along with the English translations, to be added to languages/en/en.php

An image containing a verification code is created by vf_image_u.php using GD, as follows
 <?php  
   
 /* themes/your_style/vf_image_u.php */  
   
 session_name('general_purpose');  
 session_start(); //we will store our random number inside a session variable   
      //so we need to declare a session here too.  
   
 $text = rand(10000,99999);   
 $_SESSION["vercode_u"] = $text;   
 $height = 15;   
 $width = 45;   
    
 $image_p = imagecreate($width, $height); // check about.php in case the image does not display  
 $black = imagecolorallocate($image_p, 165, 174, 112);   
 $white = imagecolorallocate($image_p, 68, 68, 68);   
 $font_size = 14;   
    
 imagestring($image_p, $font_size, 0, 0, $text, $white);   
 imagejpeg($image_p, null, 80);   
 ?>  
This simple script is using the imagecreate() function from the GD library. If the image does not display, load the about.php script that resides at root, to find out if you have GD enabled + jpeg support. We also fabricate a random number that we store inside $_SESSION[“vercode_u”].

Of course other fields can be added, some removed, that’s up to you and your project’s business logic of course, but then you need to make sure the proper fields are correctly retrieved while processing the sent data inside RegModel.php.

But from a controller point of view, this has no importance whatsoever since we will fetch our form variables programatically. Here’s how
 <?php  
   
 /* application/controllers/RegController.php */  
   
 class RegController extends BaseController{  
        
      private $module_name;  
      private $form_name;  
      private $feedback;  
                  
      public function initialize(){  
             
           $this->registry->template->module_name = $this->getModuleName();  
           $this->registry->template->form_name = $this->getFormName();  
             
           // new in our initialize() method, the intercept_data() method  
           // which will catch all form variables   
           // and send it to the relevant model method  
   
           $this->registry->template->feedback = $this->intercept_data();  
           $this->registry->template->assign_theme();  
      }  
   
      public function getModuleName(){  
           $model = RegModel::getInstance();  
           return $model->get_module_name();  
      }  
        
      public function getFormName(){  
           $model = RegModel::getInstance();  
           return $model->get_form_name();  
      }  
        
      public function intercept_data(){  
   
           // this method is where we intercept form variables  
           // to be then sent to the relevant model's method  
   
           $this->dol_posts = array();       
   
           foreach($_POST as $key => $val){//for each form variable  
   
                     // if there's a $post set of that variable,  
                     // we stack its value in the dol_posts array  
                  
                if(isset($_POST[$key])){array_push($this->dol_posts,$key);}  
           }  
             
           $model = RegModel::getInstance(); //we call an instance of the reg model  
           return $model->get_form_feedback($this->dol_posts); // and invoke the method that will process our form variables, passing them as parameter.  
      }  
 }  
 ?>  
So, this controller brings us new elements that weren’t in the page controller because the logic there was different, there was no form and thus no data to be intercepted. The intercept_data() method is where we intercept form variables to be then sent to the relevant model’s method, in this case, the get_form_feedback() method belonging to the RegModel class (Singleton).
 <?php   
   
 /* from application/models/RegModel.php */  
   
 public function get_form_feedback($parms){  
             
           $util = new UtilsModel();  
   
           if(!empty($parms)){ //if we did receive form variables  
             
                $error_list = array();  
   
                // we check for form errors  
                $error_check_reg = self::check_reg_form_errors($parms);  
                //and whether login and email are unique  
                $is_unique_login = $util->check_if_unique('users', 'user_login', $_POST[$parms[0]], array('login','already_in_use'));  
                $is_unique_email = $util->check_if_unique('users', 'user_email', $_POST[$parms[2]], array('email','already_in_use'));  
   
                //if login is not unique, we stack that info into the error array  
                if (sizeof($is_unique_login) > 0){array_push($error_list,$is_unique_login);}  
   
                //same for email  
                if (CHECK_EMAIL_UNIQUE == true && sizeof($is_unique_email) == 1){array_push($error_list,$is_unique_email);}  
   
                //and for general form errors  
                if (sizeof($error_check_reg) > 0){array_push($error_list,$error_check_reg);}  
   
                //if there's at least one error found, no matter what type            
                if(sizeof($error_list) > 0){  
                     //we return our array of errors  
                     $this->form_feedback = $error_list;            
                }  
                else{  
                     //otherwise, no form errors, so we start processing the form   
                       
                     // that's the place where you make sure the form's HTML element   
                     // names you need to pull back correspond to what's in the list just bellow  
                     $field_list = array('login','password','email','country','gender','birthdate','newsletter','vercode_u','type','ip');  
                  
                     foreach($field_list as $fl){ //and for each for variable we have fetched,   
                          //if it can be found in the array above and there's a set $post variable of that name, then  
                          //we set a variable that will bare that same name, using the variable of variable ($$) syntax  
                          if(in_array($fl,$parms) && isset($_POST[$fl])){$$fl = $_POST[$fl];}  
                     }  
   
                     //building a query to insert the new user  
                     $q_insert_user = "INSERT INTO users (user_login, user_password, user_email, user_country, user_gender, user_birthdate, user_type, user_ip) VALUES ('$login', md5('$password'), '$email', '$country', '$gender', '$birthdate', '$type', '$ip')";  
   
                     // if the query executes well  
                     if(MySQLModel::get_mysql_instance()->executeQuery($q_insert_user) == 0){  
                          //we get the ID of the newly created user  
                          $q_id = "SELECT id_user, user_login,user_password FROM users WHERE user_login = '".$login."' AND user_password = md5('".$password."')";  
                          MySQLModel::get_mysql_instance()->executeQuery($q_id);  
                          $my_id = MySQLModel::get_mysql_instance()->getRows($q_id);  
   
                          if($my_id != ''){  
                               // and if everything went well and we have that ID we wanted  
                               $id = $my_id['id_user'];  
                       
                               //let's create a settings entry in db  
                               if(CURRENT_LANG){$this->lng = CURRENT_LANG;}  
                               //do not worry about the newsletter parameter because newsletters have not really been implemented  
                               //on the project. This is here only to give an effect of form completeness  
                               if(isset($newsletter)){$this->nl = 'yes';}else{$this->nl = '';}  
                            
                               //we build the query to insert a settings row for that new user  
                               $q_insert_user_settings = "INSERT INTO user_settings(user_settings_id_user, user_settings_language, user_settings_newsletter) VALUES ('$id','$this->lng','$this->nl')";  
                                      
                               if(MySQLModel::get_mysql_instance()->executeQuery($q_insert_user_settings) == 0){  
   
                                         //and if everything went according to plan, we authentify the new user                      
                                         $auth = AuthModel::getInstance();  
                                         $auth->stay_in($id,$login,$password);  
   
                                         //and then we take care of sending a 'thanks for registering - here are your credentials...' email  
                                         // we will discuss email sending via PHPMailer in a future settlement   
                                         //so you can safely skip that part for now.  
                                         require_once('languages/templates/'.CURRENT_LANG.'/'.strtolower($this->module_name).'_'.CURRENT_LANG.'.php');  
   
                                         if($regmail = $util->send_mail(CONTACT_FORM_EMAIL,$email,$GLOBALS['t_reg_email_subject'],$GLOBALS['t_reg_email_alt'],$content)){$this->form_feedback = 0 ;}  
   
                                         //In any case, our business logic requires us to relaod the page if registration turns out successful   
                                         //simply because we want the new user to automatically see a thanks for   
                                         //registering message,  
                                         //so we build a header location link and redirect the new user to that  
                                         if(DEFAULT_PLINK != ''){  
                                              if(CLEAN_URLS == true){$plink = '/'.DEFAULT_PLINK;}  
                                              else {$plink = '&'.PLINK.'='.DEFAULT_PLINK;}  
                                         }  
                                         else{$plink = '';}  
   
                                         if(CLEAN_URLS == true){$location = CLEAN_PATH.'/'.DEFAULT_RLINK.$plink.'/'.CURRENT_LANG.'/regs';}  
                                         else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$plink.'&'.LN.'='.CURRENT_LANG.'&reg=success';}  
                                         header('Location: '.$location);exit();  
                                    }  
                               }  
                               else{$this->form_feedback = 'failed';} //self-explanatory  
                          }  
                               else{$this->form_feedback = 'failed';}  
                     }  
                }   
           return $this->form_feedback;  
      }  
 ?>  
Very simply, what that method does is, if we did intercept our form variables, we subject them to an error checking method (that we will review further bellow), we also check for the uniqueness of the login and email fields. If errors are found we stack them into an array we return to the controller, otherwise that means all came to us clean and we then start processing that data.

We insert the new user into the database and if the query went okay, we fetch its ID and insert a row into the user_settings table for that user. We log the user in, using our AuthModel class. We do send a confirmation email, but since we will discuss email sending in a future settlement only, we leave out that part for now. And since we want the new user to get a welcome message and everything, we build a location URL through which we will reload the page.

We will take a moment to review three things though, the first one being the check_reg_form_errors() method, useful to the get_form_feedback() method, as discussed above
 <?php   
   
 /* from application/models/RegModel.php */  
   
 public function check_reg_form_errors($mr){  
           $this->reg_form_errors = array();  
   
           $line = array();            
             
           foreach($mr as $r){  
   
                if(preg_match('/login/',$r)){  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_LOGIN.','.MAX_CHARS_LOGIN.'}$/',$_POST[$r])){  
                     array_push($line,'login','not_valid');array_push($this->reg_form_errors,$line);}  
                     // as an example, 'login' and 'not_valid' are the strings we will match against   
                     //while in registration.php to help us know the encountered format for the login field is not valid  
                }  
   
                if(preg_match('/password/',$r)){  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_PWD.','.MAX_CHARS_PWD.'}$/',$_POST[$r])){  
                          array_push($line,'password','not_valid');array_push($this->reg_form_errors,$line);  
                     }       
                }  
   
                if(preg_match('/email/',$r)){  
                     if (!filter_var($_POST[$r], FILTER_VALIDATE_EMAIL)){array_push($line,'email','not_valid');array_push($this->reg_form_errors,$line);}  
                     if($_POST[$r] != '' && mb_strlen($_POST[$r]) > EMAIL_MAX_LENGTH){array_push($line,'email','not_valid');array_push($this->reg_form_errors,$line);}   
                }  
   
                if(preg_match('/birthdate/',$r)){  
                     if(!empty($_POST[$r])){  
                          if(preg_match('/^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/',$_POST[$r])){$exp_d = explode('/',$_POST[$r]);  
                          //var_dump($exp_d);  
                          if(checkdate($exp_d[0], $exp_d[1], $exp_d[2]) != 1 || !preg_match('/^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/',$_POST[$r])){array_push($line,'birthdate','not_valid');array_push($this->reg_form_errors,$line);}                                     
                          }  
                          else{array_push($line,'birthdate','not_valid');array_push($this->reg_form_errors,$line);}  
                     }  
                     else{array_push($line,'birthdate','not_valid');array_push($this->reg_form_errors,$line);}  
                }  
   
                if(preg_match('/vercode_u/',$r)){  
                     if(!preg_match('/^'.$_SESSION["vercode_u"].'$/',$_POST[$r])){array_push($line,'verification_code','not_valid');array_push($this->reg_form_errors,$line);}  
                }  
   
      $line = array();  
           }  
                //var_dump($this->reg_form_errors);  
           return $this->reg_form_errors;  
      }  
 ?>  
Nothing special on that one, as it’s only about basic preg_match verification of field format. Not however that, the arrays we send back in case of found errors, correspond to the expected arrays we expect and parse in registration.php, before we display the registration form.

Second, to be able log the new user in following successful registration, we have used the stay_in() method of yet another Singleton class, the authentication class called AuthModel
 <?php   
   
 /* from application/models/AuthModel.php */  
   
 public function stay_in($id, $user, $pwd, $lang = NULL){  
      $_SESSION['c_id'] = $id;  
      $_SESSION['c_login'] = $user;  
      $_SESSION['c_pwd'] = md5($pwd);  
      $_SESSION['c_lang'] = $lang;  
 }  
 ?>  
There, we simply store obviously needed credentials into session variables, so the user stays logged in as long as it still hasn’t logged out.

Third, the get_user_ip() method from UtilsModel.php that’s being called in the registration form from registration.php
 <?php  
   
 /* from application/models/UtilsModel.php */  
   
 public function get_user_ip(){  
             
      if (!empty($_SERVER["HTTP_CLIENT_IP"])){$this->real_ip = $_SERVER["HTTP_CLIENT_IP"];}  
      elseif(!empty($_SERVER["HTTP_X_FORWARDED_FOR"])){$this->real_ip = $_SERVER["HTTP_X_FORWARDED_FOR"];}  
      else{$this->real_ip = $_SERVER["REMOTE_ADDR"];}  
             
       return $this->real_ip;  
 }  
 ?>  
Nothing complicated here, this snippet will go as far as it can to fetch the real IP of the user. Fetching someone’s IP is no perfect science, so bare with me and with this code until someone comes up with something even more efficient. Although, this here should get a fair share of users hiding behind proxies. But you got my point, it won’t be perfect in any case.

So anyway, our user can already register and upon successful registration gets logged in automatically. Now what if that same user has logged out but needs to log in again? Our system must offer that user a way to manually log in to the system. This is where the Signin module comes in handy. But again and for the sake of clarity, I will only be showing the relevant pieces of code. Indeed, the SignIn controller is identical to the RegController, except for a few occurrences where ‘Reg’ should be replaced by ‘SignIn’ and that’s all there’s is to it. So we will now get straight into the signin model’s get_form_feedback() method.
 <?php  
   
 /* from application/models/SignInModel.php */  
   
 public function get_form_feedback($parms){  
             
      if(!empty($parms)){ //if we did get our form values  
             
           $error_list = array();  
   
           //we check them against the check_signin_form_errors() method  
           $error_check_pwdc = self::check_signin_form_errors($parms);  
   
           if (sizeof($error_check_pwdc) > 0){array_push($error_list,$error_check_pwdc);}  
             
           if(sizeof($error_list) > 0){$this->form_feedback = $error_list;} // if errors were found, we return them  
           //otherwise, we authenticate the user, invoking AuthModel  
           else{$this->form_feedback = self::authentify($_POST[$parms[0]],$_POST[$parms[1]]);}  
           //0 for login and 1 for password  
      }  
   
      return $this->form_feedback;  
 }  
 ?>  
Basically here, we validate our form data against the check_signin_form_errors() model, which looks as follows
 <?php  
   
 /* from application/models/SignInModel.php */  
   
 public function check_signin_form_errors($mr){  
           $this->signin_form_errors = array();  
   
           $line = array();            
           //var_dump($mr);  
             
           foreach($mr as $r){  
           //var_dump($r);  
                if($r == 'login'){  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_LOGIN.','.MAX_CHARS_LOGIN.'}$/',$_POST[$r])){  
                          array_push($line,'login','not_valid');array_push($this->signin_form_errors,$line);  
                     }       
                }  
   
                if($r == 'password'){//echo $_POST[$r];  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_PWD.','.MAX_CHARS_PWD.'}$/',$_POST[$r])){  
                          array_push($line,'password','not_valid');array_push($this->signin_form_errors,$line);  
                     }       
                }  
   
      $line = array();  
           }  
   
           return $this->signin_form_errors;  
      }  
 ?>  
This method acts exactly the same way as check_reg_form_errors(). But if everything goes okay, we authenticate the user by calling the authentify() method from the same class.
 <?php   
   
 /* from application/models/SignInModel.php */  
   
 public function authentify($login,$password){  
      $auth = AuthModel::getInstance();  
      $this->authentified = $auth->identify($login,$password);  
   
      return $this->authentified;  
 }  
 ?>  
This method called for the identify() method kept in AuthModel.php, which looks just like this bellow
 <?php  
   
 /* from application/models/AuthModel.php */  
   
 public function identify($user,$pwd){  
             
           $default_plink = '';  
   
           if(DEFAULT_PLINK){  
                if(CLEAN_URLS == true){$default_plink = DEFAULT_PLINK.'/';}  
                else{$default_plink = '&'.PLINK.'='.DEFAULT_PLINK;}  
           }  
             
          $q_auth = "SELECT id_user,user_login,user_password   
                     FROM users   
                     WHERE user_login = '".$user."'   
                     AND user_password = md5('".$pwd."')  
                ";  
   
           MySQLModel::get_mysql_instance()->executeQuery($q_auth);  
           $my_auth = MySQLModel::get_mysql_instance()->getRows($q_auth);  
             
           if($my_auth != ''){       
                  
                $id = $my_auth['id_user'];  
                $this->identified = 'yes';  
                  
                //once the user has been authenticated, we go fetch its private language settings  
                $user_lang = "SELECT id_user_settings, user_settings_id_user, user_settings_language   
                        FROM user_settings   
                        WHERE user_settings_id_user = '".$id."'  
                        ";  
                  
                MySQLModel::get_mysql_instance()->executeQuery($user_lang);  
                  
                $userlang = MySQLModel::get_mysql_instance()->getRows($user_lang);  
   
                if($userlang != ''){$this->user_lang = $userlang['user_settings_language'];}  
   
                //and write the user to a session                 
                $this->stay_in($id, $user, $pwd, $this->user_lang);  
                  
                // IF LAST URL REMEMBERED  
                if(isset($_SESSION['last_url']) && $_SESSION['last_url'] != ''){  
                     $exp_url = explode("&",$_SESSION['last_url']);  
                     $xpr = LN.'=';  
                     $parts = '';  
                     $rep = LN.'='.CURRENT_LANG.'&';  
                       
                     foreach($exp_url as $xpurl){       
                          if(preg_match('/'.$xpr.'/',$xpurl)){  
                               if($xpurl_new = preg_replace('/'.$xpr.'/',$rep,$xpurl)){$parts .= $rep;}  
                          }  
                     else{$parts .= $xpurl.'&';}  
                     }  
                       
                     if(preg_match('/signin/',$_SESSION['last_url']) || preg_match('/fpwd/',$_SESSION['last_url'])){  
                          if(CLEAN_URLS == true){$location = CLEAN_PATH.'/'.DEFAULT_RLINK.$default_plink.'/'.CURRENT_LANG;}  
                          else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$default_plink.'&'.LN.'='.CURRENT_LANG;}                           
                     }  
                     else{$location = rtrim($parts,"&");}  
                }  
                else{  
                     if(CLEAN_URLS == true){$location = CLEAN_PATH.'/'.DEFAULT_RLINK.'/'.$default_plink.CURRENT_LANG;}  
                     else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$default_plink.'&'.LN.'='.CURRENT_LANG;}  
                }  
                header('Location: '.$location);exit();  
           }  
           else{$this->identified = 'wrong_credentials';}  
             
           return $this->identified;  
      }  
 ?>  
If you have survived these series up until now, you should not need an explanation for this last snippet, but just in case though… : once we have identified the user we go fetch its ID and language than determine the location to which we will redirect that user, based on whether a last URL was recorded and whether clean URLS are enabled or not.

Finally, now that our user has been able to log in manually, that same user may be wanting to review and update its personal data. This is just what profile module is all about. The profile module, which is alike the settings one, only differs from let’s say the signin module in that we need to get the user’s data in order to present it in the profile form.
 <?php  
      /* from application/controllers/ProfileController.php */  
   
      ...  
      // while in properties  
      private $user_data;  
      ...  
   
      ...  
      //while inside the initialize() method, after the intercept_data() method, and before assign_theme()  
      $this->registry->template->user_data = $this->getProfileFormData(); //we call the method right bellow  
      ...  
        
      private function getProfileFormData(){  
           $model = ProfileModel::getInstance();  
           return $model->getProfileFormData(); //we call and returns the method we need  
      }  
      ...  
 ?>  
and so, in the profile model we have
 <?php  
   
      /* from application/controllers/ProfileModel.php */  
   
      public function getProfileFormData(){  
             
           $this->user_profile=array();  
        
           //data for the profile module resides in the users table       
           $my_query_profile = "SELECT * FROM users WHERE id_user = '".$_SESSION['c_id']."' AND user_login = '".$_SESSION['c_login']."'";  
             
           MySQLModel::get_mysql_instance()->executeQuery($my_query_profile);  
             
                $this->up = MySQLModel::get_mysql_instance()->getRows($my_query_profile);  
                  
                if($this->up != ''){  
                     array_push($this->user_profile, $this->up['user_login'],$this->up['user_email'],$this->up['user_country'],$this->up['user_birthdate']);  
                }  
                  
                return $this->user_profile;  
      }  
 ?>  
It also differs in that once we have intercepted our form data, we need to update the database with it, and here is how we will do it
 <?php  
   
      /* from application/controllers/ProfileModel.php */  
   
      public function get_form_feedback($parms){  
             
           if(!empty($parms)){  
             
                $error_list = array();  
   
                $error_check_profile = self::check_profile_form_errors($parms);  
   
                if (sizeof($error_check_profile) > 0){array_push($error_list,$error_check_profile);}  
             
                if(sizeof($error_list) > 0){  
                     $this->form_feedback = $error_list;            
                }  
                else{            
                     $field_list = array('login','password','email','country','birthdate');  
                  
                          foreach($field_list as $fl){  
                               if(in_array($fl,$parms) && isset($_POST[$fl])){$$fl = $_POST[$fl];}  
                          }  
                  
                     $now = date("Y-m-d H:i:s");  
   
                     $q_update_profile = "UPDATE users   
                          SET user_email = '".$email."',  
                          user_country = '".$country."',  
                          user_birthdate = '".$birthdate."',  
                          user_timestamp = '".$now."'  
                          WHERE user_login = '".$login."'  
                          AND user_password = md5('".$password."')  
                          AND user_timestamp != '".$now."'  
                     ";                 
   
                     if(MySQLModel::get_mysql_instance()->executeQuery($q_update_profile) == 0){  
                          if(MySQLModel::get_mysql_instance()->affectedRows() == 1) {$this->form_feedback = 'success';}  
                          else{echo MySQLModel::get_mysql_instance()->mysql_fetch_errors();$this->form_feedback = 'wrong_password';}  
                     }  
                     else{$this->form_feedback = 'failed';}  
                }  
             
           }            
                return $this->form_feedback;  
      }  
 ?>  
You can see it’s very similar to any other get_form_feedback() methods from other models, only here we update the data. Note the use of the field user_timestamp which exists only to force MySQL to affect something if one of the values has then changed. Otherwise, if the update is performed and no rows were affected, there’s no programatic way to know it worked and so our code could only return a failed state, or nothing at best.

Regarding the profile form in profile.php, its structure appears to be no different than the registration form. You will only have to make sure your HTML form field names correspond to what is being processed in the model. On the model side, the check_profile_form_errors() methods is similar to check_reg_form_errors() so there’s no point repeating the same code over and over and besides, this will be a great exercise for you to test the knowledge you have acquired so far from these series.

By the way, let’s take a look at the user_settings table, because you will need it too
 --  
 -- Table structure for table `user_settings`  
 --  
   
 CREATE TABLE `user_settings` (  
  `id_user_settings` int(11) NOT NULL,  
  `user_settings_id_user` int(11) NOT NULL,  
  `user_settings_language` enum('de','es','fr','nl','ru','se','uk','en') NOT NULL DEFAULT 'en',  
  `user_settings_nipp` enum('10','15','20','50','75','100','150') NOT NULL DEFAULT '15',  
  `user_settings_newsletter` varchar(3) NOT NULL,  
  `user_settings_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;  
   
 --  
 -- Dumping data for table `user_settings`  
 --  
   
 INSERT INTO `user_settings` (`id_user_settings`, `user_settings_id_user`, `user_settings_language`, `user_settings_nipp`, `user_settings_newsletter`, `user_settings_timestamp`) VALUES  
 (1, 1, 'fr', '20', '', '2015-11-19 08:05:46');  
   
 --  
 -- Indexes for dumped tables  
 --  
   
 --  
 -- Indexes for table `user_settings`  
 --  
 ALTER TABLE `user_settings`  
  ADD PRIMARY KEY (`id_user_settings`);  
   
 --  
 -- AUTO_INCREMENT for dumped tables  
 --  
   
 --  
 -- AUTO_INCREMENT for table `user_settings`  
 --  
 ALTER TABLE `user_settings`  
  MODIFY `id_user_settings` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;  
Note the user_settings_nipp and newsletter fields are only here for demonstration, so you may remove them but as long as you modify your settings model accordingly of course.

Finally and regarding the forgotten password module or the change password module, there is nothing magical about them, the fpwd module is somehow very identical to the signin module and the pwdc module is relatively similar to the signin module too. Knowing that should get you on track faster while you attempt at building those new modules. Note that regarding the fpwd module, an email is sent with a new password, you may use the generate_new_pwd() method from UtilsModel.php.

Now we could not be coming full circle if we did not get in the subject of authentication again. Why? Simply because users should not be allowed to enter their profile data unless they are already logged in, which makes sense. So that, the first call inside the initialize() method of your profile controller will be to the get_module_auth() method, as described bellow
 <?php  
      /* from application/controllers/ProfileController/php */  
        
      public function initialize(){  
   
           $utils = new UtilsModel();            
           $this->registry->template->module_auth = $utils->get_module_auth();  
           ...  
      }       
 ?>  
Now the function get_module_auth() method itself, pretty much and explicit one.
 <?php  
   
      /* from application/models/UtilsModel/php */  
   
      public function get_module_auth(){  
        
           if(CURRENT_PLINK){  
                if(CLEAN_URLS == true){$pg = '/'.CURRENT_PLINK;} else{$pg = '&'.PLINK.'='.CURRENT_PLINK;}  
           }  
           else{$pg = '';}       
                       
           if(isset($_SESSION['c_login']) && isset($_SESSION['c_pwd']) && $_SESSION['c_login']!='' && $_SESSION['c_pwd']!=''){  
                $this->module_auth = 0;  
           }  
           else{  
                $this->module_auth = 1;  
                if(CLEAN_URLS == true){$location = CLEAN_PATH.'/signin/'.CURRENT_LANG;}  
                else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'=signin&'.LN.'='.CURRENT_LANG;}  
                header('Location: '.$location);exit();  
           }  
      }  
 ?>  
And that wraps up this fifth settlement dedicated to building a user interface that allows someone to register, sign in, review and update profile. From this, you can now easily extend what you have already learned to build a settings module, same as a profile module but that will rely on the user_settings table, a fpwd module that will handle cases of forgotten passwords and even a pwdc module, that will allow users to change password, first checking of course on the validity of the password they enter, then checking that the new and confirm new passwords are of correct format and match each other too.

Next time I will get into full detail as to the subject of theme and language handling, reviewing all aspects of what makes the visual identity of this project and also discuss a method for localizing strings.

This article first appeared Thursday the 7th of January 2016 on RolandC.net.

Google Analytics tracking code not being detected anymore and/or real-time tracking not working?

Wednesday, June 3, 2015

Today I am going to show you a simple trick so you can avoid a few hours of head-banging while trying to fix your Google Universal Analytics code not being detected anymore and/or erase the frustration of having a real-time feature menu taking up half the left-hand panel but that always shows only one data - '0'.


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.