Welcome to the third settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which we will be creating our first entire module, the page module.
Last time, we have set the focus on the index.php file, responsible for routing user input, calling all the needed core components that we have previously reviewed.
We have seen how the Router class, which can be assimilated as a factory, once instantiated and invoked by means of its own route() method, generates and triggers, based on user input, a new controller object that itself, aided by the __autoload() function from the index file, calls on the name-matching module class it needs in order to get all the logic and data it will use to build the needed view and it does so by relying on the $registry object that was passed onto it while being instantiated by the $router object, the $registry object containing a template object, itself containing a method to include the needed view file as well as a __set() method to store all the template variables the controller needs to pass to the view file, in order to be used by the programmer there.
So today, we are going to take a dive straight into the page module, see how its controller, model and view really work on the ground. But before we start with the page controller class, let's take a brief look at the structure of the database table called 'pages', which contains all the data we will rely on to effectively handle pages in our CMS.
Note that the field called page_name_formatted, since it corresponds to the formatted name of the page that will be either be displayed in the URL or serve inside menu links, to play it safe, will always need to contain nothing but lowercase, latin-alphabet letters and/or figures, with only underscore/minus signs allowed. Also, page_language will always have to contain two lowercase letters only.
Here is a dummy data sample for you to insert in your own page table
So now, without further waiting, here comes our PageController class
Then we call the initialize() method, the top three properties: module_name, page_data and page_name are being passed to the view, as they are being assigned the results of specific method calls to the PageModel object, by being added to the template object, which itself had been added to the registry object as a property and which also contains a __set() method allowing it to store the properties to be then passed to the view.
Finally we call the assign_theme() method from the template object. That method will prepare the above properties for display and it will include our common template files as well: header.php, main.php and footer.php.
Then remains the actual calls to some of the PageModel's methods. Those in this particular case are the calls that feed the values of our above properties, the ones we want to pass to the view. The PageModel class is a Singleton one and so we need to access and instantiate it by using the :: syntax, plus by making a call to its public and static getInstance() method. The calls to our model's methods are not made directly though. While still inside the initialize() method, we issue a call to a dedicated controller method that will itself invoke the needed model's method.
So now has come the time to lay our PageModel class down on the table.
The get_page_name() method checks if page name (CURRENT_PLINK) is set and not empty, then pulls a list of all existing pages and sees if it finds it there. If it does, it assigns the page's non-formatted and localized name to the page_name property, and if it does not, it assigns a missing page message to that same property. And if page name is not set, it assigns an empty string to page_name. For the sake of this tutorial however, you may skip checking with the existing page list and simply return the value of CURRENT_PLINK, provided it is set and not empty.
Now, in the getData() method, we set the page_data property as an array, modify the database charset to 'Latin1' and sanitize the $page parameter from the method call using PHP's native filter_var() function. Then we build a query string to pull the page table entry that matches our current page and language names and simply execute that query. In the page_data array, we stack the fields we need from the freshly pulled results. And if results there aren't, we stack a missing page message in it, along with the name of the missing page image file we will display with the message.
The page model contains fairly simple methods and its role can mostly be summed up as simply pulling content from the page table, based on page name and current language. It seems therefore fair to warn the reader on that most of the models we will soon be dealing with are not that simple in content and/or architecture. But have no fear, as we have about reviewed the core structure of the architecture we're going to be evolving in, from now the difficulty level should not be increasing that significantly. However and by nature, some of the models we will be reviewing soon will present algorithms of which the explanation might not appear to be so obvious to all readers at first. In any a case, both code comments and explanations will make up for it, so that you will never really have to worry about the growing complexity of the project.
Back one level up in our page module hierarchy, now that we have pulled the page data we needed from the DB and returned it to the controller, we're all set to call the assign_theme() method, which will transform some of our controller's properties to regular variables/arrays we can actually use inside the theme files.
Let's first take a look at a sample from main.php
And now, let's see what page.php has to tell us
As for the algorithm of the overall page template, it is very simple: if the required page is missing, we display a missing page image with a missing page string underneath, otherwise, we display the content we have pulled from the DB for the required page.
By now and by having gathered all code from this part as well as from the last one, referring to the first part for directory structure information too, you should already have a basic application that allows you to display a page based on URL input. For now though, we have only been discussing the page module.
But as an example, loading http://localhost/your_project_name/index.php?q=page&page=page_3&ln=en into your browser, if successful, should serve your screen three template files, namely header.php, main.php and footer.php, with main.php containing page.php, itself containing the DB-pulled content for page_3 (provided page_3 is not missing from the DB). If page_3 is missing from the DB, a missing image will be displayed and you are free to choose an image for yourself to work with. The page.php file can be placed everywhere it would graphically makes sense to you, as long as it stays inside main.php.
Note that for clarity reasons I have not been including header.php and footer.php in the series yet but for now, you can make up for it by adding some simple header and footer files, header.php file containing the usual html, header tags we all know about. This is where you will link your main theme CSS file, and if your style name is called pinguin, then your main CSS file would have to be called pinguin.css. The favicon file will be stored in /themes/your_style/images/favicon and be called favicon.ico. The main.php file will contain the first body tag and footer.php will contain the closing body and html tags. This way, you can start effectively styling your project while we're getting advanced but still haven't reviewed all the theme-handling aspects of our project yet. Those will be the subject of a whole forthcoming settlement.
Last but not least, in case this wasn't implicit enough: every time you encounter a global variable of the form : $GLOBAL['t_variable_name'], this means, by the very standards of this project, that this is a translation variable and that basically, in the language file pertaining to the current language (languages/current_language/current_language.php) should exist a variable called $t_variable_name = 'Translation string';. Otherwise a missing variable error is being thrown by PHP. Again, language-handling will be discussed in more details in a forthcoming settlement.
Meanwhile, do not hesitate to refer to your copy of Gumbo you have probably downloaded already last week. This will hopefully help you solidify your understanding of the project we're building together.
So this completely wraps up this settlement on how the page module works, bottom up. Next time, we will get our hands tied on yet another generally essential - not to say vital - feature in Web development, the menu system.
This article first appeared Tuesday the 5th of November 2014 on RolandC.net.
Last time, we have set the focus on the index.php file, responsible for routing user input, calling all the needed core components that we have previously reviewed.
We have seen how the Router class, which can be assimilated as a factory, once instantiated and invoked by means of its own route() method, generates and triggers, based on user input, a new controller object that itself, aided by the __autoload() function from the index file, calls on the name-matching module class it needs in order to get all the logic and data it will use to build the needed view and it does so by relying on the $registry object that was passed onto it while being instantiated by the $router object, the $registry object containing a template object, itself containing a method to include the needed view file as well as a __set() method to store all the template variables the controller needs to pass to the view file, in order to be used by the programmer there.
So today, we are going to take a dive straight into the page module, see how its controller, model and view really work on the ground. But before we start with the page controller class, let's take a brief look at the structure of the database table called 'pages', which contains all the data we will rely on to effectively handle pages in our CMS.
--
-- Table structure for table `pages`
--
CREATE TABLE IF NOT EXISTS `pages` (
`id_page` smallint(6) NOT NULL,
`page_data` mediumtext,
`page_name_formatted` varchar(60) NOT NULL,
`page_name` varchar(80) NOT NULL,
`page_language` varchar(2) NOT NULL,
`page_created` datetime DEFAULT CURRENT_TIMESTAMP,
`page_last_edited` datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `pages`
--
ALTER TABLE `pages`
ADD PRIMARY KEY (`id_page`), ADD FULLTEXT KEY `pages_data` (`page_data`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `pages`
--
ALTER TABLE `pages`
MODIFY `id_page` smallint(6) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=0;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
Note that the field called page_name_formatted, since it corresponds to the formatted name of the page that will be either be displayed in the URL or serve inside menu links, to play it safe, will always need to contain nothing but lowercase, latin-alphabet letters and/or figures, with only underscore/minus signs allowed. Also, page_language will always have to contain two lowercase letters only.
Here is a dummy data sample for you to insert in your own page table
INSERT INTO 'pages' ('id_page', 'page_data', 'page_name_formatted', 'page_name', 'page_language', 'page_created', 'page_last_edited') VALUES
(1, 'data for page 1', 'page_1', 'Page 1', 'en', '2014-09-06 23:05:39', '2014-09-06 23:53:19'),
(2, 'data for page 2', 'page_2', 'Page 2', 'en', '2014-09-06 23:06:42', '2014-09-06 23:55:28'),
(3, 'data for page 3', 'page_3', 'Page 3', 'en', '2014-09-06 23:07:18', '2014-09-06 23:58:02')
);
So now, without further waiting, here comes our PageController class
<?php
/* application/controllers/PageController.php */
class PageController extends BaseController{
//Declaring our PageController class
//This class benefits from the properties of the abstract parent class it extends, called BaseController,
//but even though the parent class forces us to have an initialize() method set here,
//it is up to us programmers to decide how we wish to implement it here.
//declaring our private properties
private $module_name; //passed to main.php to validate the including of page.php
private $page_data; // passed to page.php as containing the actual data stored in the DB for the particular page the user seeks
private $page_name; //passed to header.php to serve inside the meta title tag
private $page; // only this property will not be passed directly to the view,
//instead, it will hold the value of plink and be passed as a parameter to the model's get_data() method
//Note: those properties will not be passed as-is to the view, not from here at least, so we can confidently keep them as private while here.
public function initialize(){ // this is the method our abstract BaseController class forces us to declare
//Next we issue a few calls to some methods that will in turn call upon the model to get the needed data
//and we store the results as properties of the template object (remember the Template class contains a __set() method too)
//The template object is being itself stored as a property of the registry object
$this->registry->template->module_name = $this->getModuleName();
$this->registry->template->page_data = $this->getPageData();
$this->registry->template->page_name = $this->getPageName();
//note the $this keyword which specifies that we refer to this class only
//finally, we call the assign_theme() method from the template object,
//that method will take care off preparing the above properties for display,
//plus it will include our common template files (header, main and footer)
$this->registry->template->assign_theme();
}
public function getModuleName(){
// we instantiate our PageModel class, which is Singleton, so we need to use the :: syntax and call its getInstance() method to do so.
$model = PageModel::getInstance();
//and we simply call its get_module_name() method, which will simply return the name of the module, as a string.
return $model->get_module_name(); //We return than name
}
public function getPageName(){
// same operation with the get_page_name() method, which will return
// the non-formatted localized name of the page from the DB
$model = PageModel::getInstance();
return $model->get_page_name();
}
public function getPageData(){
if(CURRENT_PLINK){$this->page = CURRENT_PLINK;}
else{$this->page = DEFAULT_PLINK;}
// we assign whatever plink value we have fetched in the index
// otherwise we assign the default plink value
$model = PageModel::getInstance(); //we instantiate our PageModel class again
return $model->getData($this->page,CURRENT_LANG);
// and call the getData() method (passing the URL page name and current language as parameters),
// which will fetch the page content from the DB for us. We return the result of the call
}
}
?>
We start by declaring our PageController class and declare the few properties we will need along the way. Only the top three properties will be passed to the view.
Then we call the initialize() method, the top three properties: module_name, page_data and page_name are being passed to the view, as they are being assigned the results of specific method calls to the PageModel object, by being added to the template object, which itself had been added to the registry object as a property and which also contains a __set() method allowing it to store the properties to be then passed to the view.
Finally we call the assign_theme() method from the template object. That method will prepare the above properties for display and it will include our common template files as well: header.php, main.php and footer.php.
Then remains the actual calls to some of the PageModel's methods. Those in this particular case are the calls that feed the values of our above properties, the ones we want to pass to the view. The PageModel class is a Singleton one and so we need to access and instantiate it by using the :: syntax, plus by making a call to its public and static getInstance() method. The calls to our model's methods are not made directly though. While still inside the initialize() method, we issue a call to a dedicated controller method that will itself invoke the needed model's method.
So now has come the time to lay our PageModel class down on the table.
<?php
/* application/models/PageModel.php */
class PageModel{ //declaring our PageModel class
static $instance; // needed to help make the class a Singleton one
private $module_name;
private $pageData;
private $page_name;
public static function getInstance(){ //method to create a single static instance of this class,
//which will be accessed from anywhere outside the class using the :: operator.
//It can be called anything you wish.
if(self::$instance == null){ // if no instance was created
self::$instance = new self(); // we create one
//the keyword 'self' reffers to the current class
}
return self::$instance; //returning the newly created instance
}
private function __construct(){}
private function __clone(){}
//Those two methods are here to prevent the programmer from creating a second instance of this class by using the new() or clone() methods.
//Only one instance of this class will be accessible and it will be accessible using the getInstance() method only.
//This is how Singleton classes are built.
public function get_module_name(){ //the method that will set and return the module name we need in main.php
//If $module_name has been set and passed to main.php, then page.php will be included
//in main.php, where we need it to graphically appear.
$this->module_name = 'Page'; //setting the value of the module_name property,
//which has not yet been turned to a variable by the template object
return $this->module_name; //returning that value
}
public function get_page_name(){
//method to get the non-formatted, localized name of the page
if(CURRENT_PLINK){
// if plink has been set in the URL and is not empty
//we instantiate MenuModel (also a Singleton)
$menu_instance = MenuModel::getInstance();
$page_list = $menu_instance->getTreeMenuItemTranslations(CURRENT_LANG,'page');
//and fetch the list of all existing pages found in DB 'pages' table.
//This is a method that originally serves other purposes,
//but since part of the data it returns contains all the data we need here,
//we save ourself writing an extra method by using it.
$cv = 0; //we initialize a counter
foreach($page_list as $pl){ foreach known front-end page
if(!in_array(CURRENT_PLINK,$pl)){$cv++;}
//if our page name cannot be found inside the currently
//looped page from the DB, we increment $cv by 1.
else{
//otherwise, that means our page name is valid
if($pl[1] == CURRENT_LANG){$this->page_name = $pl[2];
//and once the current language is being matched, we assign
//the corresponfing non-formatted page name to the page_name property
}
}
}
if($cv == sizeof($page_list)){ // if the page name could not be found while looping the array above,//we build a missing link instead and assign it to the to the page_name property
$this->page_name = CURRENT_PLINK.' ('.$GLOBALS['t_'.CURRENT_LANG.') - '.$GLOBALS['t_page_missing'];
}
}
else{$this->page_name = ''; //if no page name was set, we assign it an empty string value}
return $this->page_name; //we return the page name, whatever its value
}
public function getData($page, $ln){ //we declare the function, taking page name and language as parameters
$this->pageData = array(); //we declare the array that will hold our page data from the DB
$page = filter_var($page,FILTER_SANITIZE_MAGIC_QUOTES); //we use the PHP native filter_var function to sanitize our page name.
//This is primarily a matter of security as we don't want issues related to SQL Injection attacks
//by means of manually (deliberately?) placing quotes or double quotes in the URL to or in place of the page name.
MySQLModel::get_mysql_instance()->set_char('latin1'); //we invoke our MySQL library's set_char() method
//to avoid encoding issues with the content we pull here. Latin1? yes, more on that in a future settlement though.
$my_query = "SELECT id_page, page_name, page_data
FROM pages WHERE page_name_formatted = '".$page."'
AND page_language = '".$ln."'
";
//we build a query in which we select all entries of which the formatted named and language correspond respectively to our current page and language names. Only one single row should be found as a result.
MySQLModel::get_mysql_instance()->executeQuery($my_query);
// we instantiate our MySQL instance and execute the query
$res = MySQLModel::get_mysql_instance()->getRows($my_query);
// and we pull what's been found, assign it to $res
if($res != ''){ //If what's been found isn't empty
array_push($this->pageData, $res['page_data'], $res['page_name']); //we stack it insde the pageData array
}
else{ //otherwise
if(isset($page) && $page != ''){ $page = stripslashes($page).' ('.$GLOBALS['t_'.$ln].')';}
else{$page = '';}
//we stack a missing page message in the pageData array,
//that will include the image file name of the missing page sign we will display in the view.
array_push($this->pageData, $page.$GLOBALS['t_page_missing'],'missing_page2.png');
}
return $this->pageData; //and we return whatever we have picked up and stacked in the pageData array.
}
}
?>
The PageModel class is a Singleton one. I have described in the comments the way a Singleton class can be built, by declaring a public static function that will create a single static instance of the class every time it will be called. But, to remain a Singleton, the class needs to be declared empty __construct() and __clone() methods so that a second instantiation cannot be initiated by the programmer, even mistakingly.
The get_page_name() method checks if page name (CURRENT_PLINK) is set and not empty, then pulls a list of all existing pages and sees if it finds it there. If it does, it assigns the page's non-formatted and localized name to the page_name property, and if it does not, it assigns a missing page message to that same property. And if page name is not set, it assigns an empty string to page_name. For the sake of this tutorial however, you may skip checking with the existing page list and simply return the value of CURRENT_PLINK, provided it is set and not empty.
Now, in the getData() method, we set the page_data property as an array, modify the database charset to 'Latin1' and sanitize the $page parameter from the method call using PHP's native filter_var() function. Then we build a query string to pull the page table entry that matches our current page and language names and simply execute that query. In the page_data array, we stack the fields we need from the freshly pulled results. And if results there aren't, we stack a missing page message in it, along with the name of the missing page image file we will display with the message.
The page model contains fairly simple methods and its role can mostly be summed up as simply pulling content from the page table, based on page name and current language. It seems therefore fair to warn the reader on that most of the models we will soon be dealing with are not that simple in content and/or architecture. But have no fear, as we have about reviewed the core structure of the architecture we're going to be evolving in, from now the difficulty level should not be increasing that significantly. However and by nature, some of the models we will be reviewing soon will present algorithms of which the explanation might not appear to be so obvious to all readers at first. In any a case, both code comments and explanations will make up for it, so that you will never really have to worry about the growing complexity of the project.
Back one level up in our page module hierarchy, now that we have pulled the page data we needed from the DB and returned it to the controller, we're all set to call the assign_theme() method, which will transform some of our controller's properties to regular variables/arrays we can actually use inside the theme files.
Let's first take a look at a sample from main.php
<?php
/* themes/your_theme_name/main.php */
...
<td>
<?php
try{ //we do use a try/catch bloc with this kind of include
if(isset($module_name) && !empty($module_name)){
//if the controler has passed us the module_name property
//and provided it's not empty, we then use it to include the corresponding view file
$path = PATH_TO_THEMES.'/'.CURRENT_THEME.'/'.strtolower($module_name).'.php';
include $path;
}
else {
//else we throw an exception containing whatever message we need to visually display
throw new Exception('<p> </p><p> </p><div class="loading_tpl_fails">'.$GLOBALS['t_the_theme_named'].' <u>'.$module_name.'</u> '.$GLOBALS['t_couldnt_be_found'].'</div>');
}
}
catch(Exception $e){
echo $e->getMessage();
exit(0);
}
?>
</td>
...
?>
The main.php file is the place where page.php will actually be included, as again, it will not be included straight from the assign_theme() method of the template object. We do use a try/catch bloc to wrap this include statement.
And now, let's see what page.php has to tell us
<?php
/* themes/your_theme_name/page.php */
<?php
// See how we find back our controller properties here,
// transformed to regular variables or arrays, holding the value(s) we have assigned them
// while still in the initialize() method of the controller
if(isset($page_data[1]) && preg_match('/missing_page/',$page_data[1])){ //if the required page is missing
?>
<img src="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES.'/'.CURRENT_THEME.'/'; ?>images/servicing/<?php echo $page_data[1]; ?>" />
//we display a missing page image (remember we stacked the image
//file name inside the page_data array while in the model
//then returned it to the controller and passed it onto the view here)
<p> </p>
<?php
}
if(isset($page_data[0]) && is_string($page_data[0]) && !preg_match('/missing_page/',$page_data[0]) ){
//if the required page is not missing and its value does not match 'missing_string'
?>
<div align="center">
<?php
print $page_data[0]; //we print the data we had stacked in the page_data array
?>
</div>
<?php
}
?>
<br />
?>
We can now clearly see how we find back and use our controller properties while inside the view, eg: page_data, transformed to regular, usable PHP array, holding the value(s) we assigned it while we were still in the initialize() method of the controller.
As for the algorithm of the overall page template, it is very simple: if the required page is missing, we display a missing page image with a missing page string underneath, otherwise, we display the content we have pulled from the DB for the required page.
By now and by having gathered all code from this part as well as from the last one, referring to the first part for directory structure information too, you should already have a basic application that allows you to display a page based on URL input. For now though, we have only been discussing the page module.
But as an example, loading http://localhost/your_project_name/index.php?q=page&page=page_3&ln=en into your browser, if successful, should serve your screen three template files, namely header.php, main.php and footer.php, with main.php containing page.php, itself containing the DB-pulled content for page_3 (provided page_3 is not missing from the DB). If page_3 is missing from the DB, a missing image will be displayed and you are free to choose an image for yourself to work with. The page.php file can be placed everywhere it would graphically makes sense to you, as long as it stays inside main.php.
Note that for clarity reasons I have not been including header.php and footer.php in the series yet but for now, you can make up for it by adding some simple header and footer files, header.php file containing the usual html, header tags we all know about. This is where you will link your main theme CSS file, and if your style name is called pinguin, then your main CSS file would have to be called pinguin.css. The favicon file will be stored in /themes/your_style/images/favicon and be called favicon.ico. The main.php file will contain the first body tag and footer.php will contain the closing body and html tags. This way, you can start effectively styling your project while we're getting advanced but still haven't reviewed all the theme-handling aspects of our project yet. Those will be the subject of a whole forthcoming settlement.
Last but not least, in case this wasn't implicit enough: every time you encounter a global variable of the form : $GLOBAL['t_variable_name'], this means, by the very standards of this project, that this is a translation variable and that basically, in the language file pertaining to the current language (languages/current_language/current_language.php) should exist a variable called $t_variable_name = 'Translation string';. Otherwise a missing variable error is being thrown by PHP. Again, language-handling will be discussed in more details in a forthcoming settlement.
Meanwhile, do not hesitate to refer to your copy of Gumbo you have probably downloaded already last week. This will hopefully help you solidify your understanding of the project we're building together.
So this completely wraps up this settlement on how the page module works, bottom up. Next time, we will get our hands tied on yet another generally essential - not to say vital - feature in Web development, the menu system.
This article first appeared Tuesday the 5th of November 2014 on RolandC.net.