Welcome to this second settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which we will be discussing the entry point of our application: The index.php file.
During the first part, we have, among other things, reviewed the core principles around which we will develop our project and also briefly introduced its directory structure.
But now, all those would be totally useless if not for one single file, the one and only entry point of our application, the index file, responsible for handling user input, requiring all the needed components along the way and which we are now going to decorticate, step by step.
Before we start: I have based these series entirely on Gumbo-CMS, the new Multilingual PHP5 MVC CMS in town, which I have recently released under the GPL V2 License. So feel free to download yourself a copy before we actually get started, as it will contain every single snippet of code you will be able to find throughout the entire tutorial. This will provide you with some guidance if you ever get lost during one of the forthcoming settlements. Equally, you will find useful insights as to which kind of feature Gumbo-CMS offers by taking a look at its Product Overview PDF.
So, we jump right in by declaring a session on line 2, just below the opening php tag and we also include our config file.
RLINK, implicitly meaning Router Link, is the constant that holds the $_GET name of a module name, be it current or sought after. Equally important are PLINK and LN, which will respectively hold page and language names.
The value of all URL constants can be modified to taste, as long as the CLEAN_URLS constant is set to false. This because of the rewrite rules in .htaccess that will not allow the dynamic naming of target variables within its rules. But as long as clean URLS aren't enabled, you are free to give any value you wish to your URL $_GET variables.
Let's break away momentarily from the index.php file to take a look at this extract from config.php
And now has come the time to call and instantiate some of our core classes, namely the router, registry and template classes.
We call on the registry, which contains magic __set() and __get() methods: this will simply provide a registry to our application in which to store some of our application variables.
We also call and instantiate the template class, adding it to our registry object as one of its properties and which contains our template engine. It will store both the variables assigned by the controller for the view to use and include the view itself.
Singleton? Yes. Design patterns, in the software development industry, are patterns that have been observed, tried and accepted as being the most likely solutions to a set of commonly encountered problems, so that you do not have to reinvent the wheel and implementing solutions to commonly encountered patterns is somehow the best way you can use to make sure you're being on the right track, at least on the same track as most others.
The Singleton pattern is a very popular software design pattern that's very useful in our case. Our MySQLModel class does not need and shall not be instantiated more than once, all while it will still be able to perform its duties CMS-wide.
Having made our MySQLModel class a Singleton one allows us, once loaded, to access it from anywhere using the :: syntax, thus we do not have to store our database connection inside a global variable, a session variable or whatever else.
Using the MySQLModel::get_mysql_instance()->newConnection(params) syntax, we simply instantiate our class and add a call to the newConnection() method altogether, passing our database connection constants from the config file as parameters. Since the call is made early enough in our index file, we do not need to perform any additional connection anywhere else. We will only need to use the MySQLModel::get_mysql_instance()->method_name(params); syntax every time we need to perform an operation with MySQL.
Note that even though we have named our class the MySQL model, we do connect to MYSQL via the mysqli extension.
Next, we engage in pre-routing user input, or what remains of it. Up to Gumbo-CMS v0.96beta (included), pre-routing was operated by means of header redirects, under the logic that we always wanted to have a valid module link (RLINK) and a valid language link (LN) defined at a minimum in the URL, at any time.
Now bare with me, Google does not like header redirects, even though they claim 302 status is fine for them, so you will have all the trouble of the world trying to index your site using such (security?) procedure. While the Bing engine will index your code just fine, Google's spiders will not because they see these kinds of redirects as hijacking attempts. Which is why v.97 features a new pre-routing approach which does not use PHP headers at all.
In this new approach, we will simply determine a value for PLINK, RLINK and LN and assign those to constants, respectively CURRENT_PLINK, CURRENT_RLINK and CURRENT_LANG
We do just the same for RLINK, except we will confront it to a menu type list found in config.php under the name of MENU_TYPE_LIST and which contains all the valid module names in use on the front-end part. If a module is set in the URL and can be found in the menu list, we then assign it to $rlink. Otherwise $rlink takes the default rlink constant value, DEFAULT_RLINK. We again create a constant to hold that value, which we name CURRENT_RLINK.
LocModel, also Singleton, is a very useful class that contains a few methods that will help us manage languages effectively. We will be using it here to fetch all the available languages.
We then fetch the default language and store it into $dlang and we then also fetch a list of our available languages, which we process to a new array called $available_language_codes.
Now, if a language was set in the URL and is found valid, by format and reference to our existing language list, we do use it.
Else, if a Google-owned language URL parameter is set (namely $_GET['language']), we will use it instead. This will happen only when using the search module (powered by Goggle CSE), while in two-column view.
Else again, if we have a language session variable set, we make use of it, otherwise, and only otherwise, we try the default language that we have fetched a little earlier
In any case, passed that point, we do have a valid module name, page name (empty or not), and language name, so that, we can route the proper translation files, using a try/catch bloc as follows :
And then we pull our current theme from the database
And now time to take care off the user wanting to log out
Then comes somehow the most important call of our index file, the call to the router's route() method.
This is exactly where we go deeper in trying to understand the core mechanisms that will make a module load upon being requested via the URL, be it a module to handle the serving of content, a module to allow the user to sign in or a module to manage some options in the back-end, it all loads from one object only - the router object.
In fact, back to design patterns, the very pattern that we will encounter now is called the factory pattern.
The factory pattern maybe be defined as a pattern in which the factory object creates other objects. This has a number of obvious benefits, such as the centralizing of the process of creating new objects. Therefore, if you need to change anything to the way you create and instantiate your new objects, then you only have to do so inside the factory.
In our case, the factory is our router. Our router, based on automatic URL parsing, will programmatically instantiate the controller of the module the client/user has asked for, and set it to work it by calling its initialize() method.
Just like with a regular factory. The client company needs a product, sends an order form, the order being our URL, the marketing people take the order form and tear down only the relevant page where the needed product is described, and handle it to the main plant people which in turn create the product and initialize it.
The __construct() method of our router class would be the order handling department, and the route method() would be the product building plant, in which is passed, as a parameter, the registry object we have created earlier, to be passed on, as a parameter again, to the controllers while being instantiated.
The registry works much as a stocking unit or a phone directory if you prefer, in the sense that you can store objects in it, to be later pulled out whenever required. How is that done? by using magic set() and get() methods, that will be triggered internally every time we will try to write/read to/from inaccessible properties, instead of having PHP generate an error.
In the __get() method, which is run when accessing data to from inaccessible properties, we simply return the value of a given array vars index by passing it as a parameter.
But back to our Router class
Now onto the route() method, we bring our last core class, the base class, by including an abstract baseController class inside the router. The soon-to-be instantiated controller class will extend that BaseController class, so that it will inherit from its protected registry property, however, we as programmers will be free to decide how we want to implement the initialize() method that baseController is imposing on us. Note that as an abstract class, BaseController cannot be instantiated.
Whatever it has included, it instantiates. So now we have a controller object ready.
Further on, if that controller object's initialize() method is found callable, our router will call it. The initialize() method is where we will define the actions we want each controller to execute for itself and have it write some properties to the registry's template object to be passed directly to the corresponding view, in order to be used there. Within that method, we will also have each controller call the assign_theme() method of the registry's template object it holds.
This is how our user input is being routed. But then, we have to find and instantiate the model that corresponds to each created controller too. Because the models are the essential component that will provide our application with the business logic and data it needs. So, this is where the magic __autoload() function comes handy
But what of the view then? As we said earlier, the view files, which are in fact basic php files acting as templates, will be included by the template object's assign_theme() method, itself called in the controller's initialize() method.
So here, in essence, we already have a full-blown MVC application in our hands. A router (factory) that loads the needed controller based on user input, a magically loaded model to provide, through its own set of properties and methods, for all the logic and data our application requires and a view file, fed by the template engine, itself containing a magic set() method and being contained in the registry object that was passed on to the controller as a parameter while being instantiated and that includes, at the controller's request, all the views needed for display, plus, that also stores, by means of set() methods, and displays all the data the controller wishes to handle to the view.
In the next settlement, we will set the focus on the controller, the model and the view, on how they are built, work together and we will come full circle by creating our first complete module, the page module. Do not miss it!
This article first appeared on Tuesday the 28th of October 2014 on RolandC.net.
During the first part, we have, among other things, reviewed the core principles around which we will develop our project and also briefly introduced its directory structure.
But now, all those would be totally useless if not for one single file, the one and only entry point of our application, the index file, responsible for handling user input, requiring all the needed components along the way and which we are now going to decorticate, step by step.
Gumbo-CMS is the application on which these series are based |
Before we start: I have based these series entirely on Gumbo-CMS, the new Multilingual PHP5 MVC CMS in town, which I have recently released under the GPL V2 License. So feel free to download yourself a copy before we actually get started, as it will contain every single snippet of code you will be able to find throughout the entire tutorial. This will provide you with some guidance if you ever get lost during one of the forthcoming settlements. Equally, you will find useful insights as to which kind of feature Gumbo-CMS offers by taking a look at its Product Overview PDF.
So, we jump right in by declaring a session on line 2, just below the opening php tag and we also include our config file.
<?php
//we initiate a session, useful for user sessions handling,
// last visited URLs, etc.
session_start();
//and we include our config file
include 'config.php';
Next, we instantiate the UtilsModel class, which contains a few useful 'all-purpose' functions. // we instantiate our general-purpose class called UtilsModel
$utils = new UtilsModel();
Then we define our timezone.
date\_default\_timezone_set(DDTS); // we define our timezone
DDTS will be the config constant that holds our timezone value, eg: Europe/Stockholm, Europe/Paris. This is required since PHP 5.10 because otherwise, I quote from the php.net site : 'every call to a date/time function will generate a E_NOTICE if the timezone isn't valid, and/or a E_WARNING message if using the system settings or the TZ environment variable'. Alternatively, you can also set the date.timezone parameter in php.ini.
/*recording the last visited url*/
if(isset($_GET[RLINK]) && $_GET[RLINK] != 'signin' && $_GET[RLINK] != 'fpwd' && $_GET[RLINK] != 'fpwd'){
//if a module is set and that is neither the signin model nor the forgotten password one, we store the request URI in a session variable
$_SESSION['last_url'] = $_SERVER['REQUEST_URI'];
}
This above is a snippet that pertains to the logic of our CMS, in regard to the recording of the last visited URL. This is also a good occasion to start introducing the URL constant naming scheme we will be using throughout the whole project while developing it.RLINK, implicitly meaning Router Link, is the constant that holds the $_GET name of a module name, be it current or sought after. Equally important are PLINK and LN, which will respectively hold page and language names.
The value of all URL constants can be modified to taste, as long as the CLEAN_URLS constant is set to false. This because of the rewrite rules in .htaccess that will not allow the dynamic naming of target variables within its rules. But as long as clean URLS aren't enabled, you are free to give any value you wish to your URL $_GET variables.
Let's break away momentarily from the index.php file to take a look at this extract from config.php
/* config.php */
...
define("CLEAN_URLS",false); //true or false. true to enable it, false to disable - NOTE : mod_rewrite has to be enabled for it to work. We will set it to true when we reach our settlement about setting up clean URL's, so for now, let's keep it disabled
/*routing*/
define("RLINK","q"); //Router Link- var that holds the current module name in $_GET requests - make sure it's different from the other url variables found in this file, and different from google cse reserved terms (cx, language, sa, etc...) - use 'ctrl + F' to check within this file
define("DEFAULT_RLINK","page"); // Warning: if you choose a default module that's different from the page module,
define("DEFAULT_PLINK","formatted_name_of_the_page_you_wish"); //Page Link // then you will have to leave the DEFAULT_PLINK value blank;
define("PLINK","p");
...
But back to our little snippet in the index (commented as 'recording the last visited url'), what it says is: If a module is set and that module is different from the signin module and different from the forgotten password module too, we then store the REQUEST URI in a session variable we call $_SESSION['last_url']. We do this because we simply don't want to have the user automatically come back to the login screen or the forgotten password procedure once it has successfully logged in.And now has come the time to call and instantiate some of our core classes, namely the router, registry and template classes.
/* requiring the core clases */
require_once('application/Router.php');
require_once('application/Registry.php');
require_once('application/Template.php');
/* instantiating them */
$router = new Router();
$registry = new Registry();
$registry->template = new Template(); // here we add the new template object to the registry object, as one of its properties
The router will do the work of routing user input by retrieving the requested GET variable, then including and calling the corresponding controller, based on the value of that variable. We will review what the router actually does in detail when the route() function is called later in the the file. We call on the registry, which contains magic __set() and __get() methods: this will simply provide a registry to our application in which to store some of our application variables.
We also call and instantiate the template class, adding it to our registry object as one of its properties and which contains our template engine. It will store both the variables assigned by the controller for the view to use and include the view itself.
/* DB connection initiating */
MySQLModel::get_mysql_instance()->newConnection(DBHOST,DBUSER,DBPWD,DBNAME,DBPORT);
// we instantiate our Singleton MySQLModel class and invoke the newConnection() method, passing our config's db settings as parameters
To connect to and interact with the MySQL database, we will use a custom MySQLModel class, which I have found on the Internet and turned into a Singleton class, so its instantiation gets restricted to one object only and which we will be able to access via the get_mysql_instance() method, every time we need to perform an operation with the database. Singleton? Yes. Design patterns, in the software development industry, are patterns that have been observed, tried and accepted as being the most likely solutions to a set of commonly encountered problems, so that you do not have to reinvent the wheel and implementing solutions to commonly encountered patterns is somehow the best way you can use to make sure you're being on the right track, at least on the same track as most others.
The Singleton pattern is a very popular software design pattern that's very useful in our case. Our MySQLModel class does not need and shall not be instantiated more than once, all while it will still be able to perform its duties CMS-wide.
Having made our MySQLModel class a Singleton one allows us, once loaded, to access it from anywhere using the :: syntax, thus we do not have to store our database connection inside a global variable, a session variable or whatever else.
Using the MySQLModel::get_mysql_instance()->newConnection(params) syntax, we simply instantiate our class and add a call to the newConnection() method altogether, passing our database connection constants from the config file as parameters. Since the call is made early enough in our index file, we do not need to perform any additional connection anywhere else. We will only need to use the MySQLModel::get_mysql_instance()->method_name(params); syntax every time we need to perform an operation with MySQL.
Note that even though we have named our class the MySQL model, we do connect to MYSQL via the mysqli extension.
Next, we engage in pre-routing user input, or what remains of it. Up to Gumbo-CMS v0.96beta (included), pre-routing was operated by means of header redirects, under the logic that we always wanted to have a valid module link (RLINK) and a valid language link (LN) defined at a minimum in the URL, at any time.
Now bare with me, Google does not like header redirects, even though they claim 302 status is fine for them, so you will have all the trouble of the world trying to index your site using such (security?) procedure. While the Bing engine will index your code just fine, Google's spiders will not because they see these kinds of redirects as hijacking attempts. Which is why v.97 features a new pre-routing approach which does not use PHP headers at all.
In this new approach, we will simply determine a value for PLINK, RLINK and LN and assign those to constants, respectively CURRENT_PLINK, CURRENT_RLINK and CURRENT_LANG
/* pre-routing (or what's left of it) */
$plink = '';
// if there is a $_GET[PLINK] set, let's use it
if(isset($_GET[PLINK])){$plink = $_GET[PLINK];}
// otherwise let's use DEFAULT_PLINK
else{$plink = DEFAULT_PLINK;}
// whatever what set, we assign it to CURRENT_PLINK, to use it wherever needed, whenever needed
define("CURRENT_PLINK",$plink);
$rlink = ''; //same here for RLINK, with the difference that :
// we use a module name list stored in config.php to check if that
// if we have a module set in URL, it has to be a valid module name
$exp_menu_type_list = explode(";", MENU_TYPE_LIST);
if(isset($_GET[RLINK]) && in_array($_GET[RLINK], $exp_menu_type_list) ){$rlink = $_GET[RLINK];}
// otherwise, we use the default RLINK value, namely DEFAULT_RLINK
else{$rlink = DEFAULT_RLINK;}
// same as for PLINK, we store what we have in a constant
define("CURRENT_RLINK",$rlink);
$dlang = $utils->get_default_lang(); //we need to fetch the default language
$available_languages = LocModel::getInstance()->getData_language_bar(); // we also need a list of the currently existing language in the database
// Since the available language list we've just fetched contains more than what we need
// we create an array in which we will store just the elements we seek
$available_language_codes = array();
foreach($available_languages as $a_l){array_push($available_language_codes, $a_l[0]);}
// Now, if a language is set in the URL, AND is a valid language (made of only 2 lowercase letters)
// AND can be traced in our available language codes array we've just filled in,
// then it's all a valid language that we will store in $langf
if(isset($_GET[LN]) && preg_match('/^[a-z]{2}$/',$_GET[LN]) && in_array($_GET[LN], $available_language_codes)){$langf = $_GET[LN];}
// else if Google's language URL variable is set (we will have to rely on it while using the search module)
// we use it
elseif(isset($_GET['language']) && preg_match('/^[a-z]{2}$/',$_GET['language']) && in_array($_GET['language'], $available_language_codes) ){$langf = $_GET['language'];}
// else if a session language variable is set, we use it
elseif(isset($_SESSION['C_LANG'])){$langf = $_SESSION['C_LANG'];}
// else we assign the default language we fetch a little earlier
else{$langf = trim($dlang);}
// and define a CURRENT_LANG constant to assign it whatever value we have just fetched
define("CURRENT_LANG",$langf);
// we create a language session variable, that will help in cases when a language was selected other than the default one and the user/visitor reloads the home link with no parameter, so the current language is not set back to default.
$_SESSION['C_LANG'] = $langf;
So, here we first take care off PLINK - if it wasn't set, we use the default value found in config.php (DEFAULT_PLINK), assign what we have to CURRENT_PLINK, that we create. We do just the same for RLINK, except we will confront it to a menu type list found in config.php under the name of MENU_TYPE_LIST and which contains all the valid module names in use on the front-end part. If a module is set in the URL and can be found in the menu list, we then assign it to $rlink. Otherwise $rlink takes the default rlink constant value, DEFAULT_RLINK. We again create a constant to hold that value, which we name CURRENT_RLINK.
LocModel, also Singleton, is a very useful class that contains a few methods that will help us manage languages effectively. We will be using it here to fetch all the available languages.
We then fetch the default language and store it into $dlang and we then also fetch a list of our available languages, which we process to a new array called $available_language_codes.
Now, if a language was set in the URL and is found valid, by format and reference to our existing language list, we do use it.
Else, if a Google-owned language URL parameter is set (namely $_GET['language']), we will use it instead. This will happen only when using the search module (powered by Goggle CSE), while in two-column view.
Else again, if we have a language session variable set, we make use of it, otherwise, and only otherwise, we try the default language that we have fetched a little earlier
In any case, passed that point, we do have a valid module name, page name (empty or not), and language name, so that, we can route the proper translation files, using a try/catch bloc as follows :
/* routing translations now */
try{
$ln_file = PATH_TO_LANGUAGES.'/'.CURRENT_LANG.'/'.CURRENT_LANG.'.php';
if (file_exists($ln_file)){include ($ln_file);} // if our language file exists, we include it
//otherwise we throw and exception
else {throw new Exception('<p> </p><p> </p><div class="loading_langfile_fails"><u>' .$_GET[LN].'.php </u> cannot be found</div>');}
}
catch(Exception $e){
echo $e->getMessage(); //which we catch and echo here
exit(0);
}
This snippet is pretty much self explanatory. If the language file we are trying to include exists, we do include it, otherwise we throw and exception containing whatever error message we need to display.And then we pull our current theme from the database
//we first define our query
$q_tpl = "SELECT id_frontend_settings, frontend_settings_name, frontend_settings_value
FROM frontend_settings
WHERE frontend_settings_name = 'theme'
";
MySQLModel::get_mysql_instance()->executeQuery($q_tpl); //we call on our MySQLModel to execute this query
if($myrows = MySQLModel::get_mysql_instance()->getRows($q_tpl)){ //if a result is found
define("CURRENT_THEME",$myrows['frontend_settings_value']); //we define a constant named CURRENT_THEME and we assign it the value the extract from the returned result
}
We simply store our freshly-pulled theme name in the constant named CURRENT_THEME, which we will use throughout the CMS wherever necessary.And now time to take care off the user wanting to log out
$auth = AuthModel::getInstance(); we instantiate our Singleton authentification class
if(isset($_SESSION['c_login']) && isset($_SESSION['c_pwd'])){ if the user is logged in and wanting to log out, we invoke our let_out() method from the Authmodel class
if(isset($_GET[LETOUT]) && $_GET[LETOUT] == 'yes'){
$auth->let_out($_SESSION['c_login'],$_SESSION['c_pwd']);
}
if(preg_match('/\/out/',$_SERVER['REQUEST_URI'])){
$auth->let_out($_SESSION['c_login'],$_SESSION['c_pwd']);
}
}
We store an instance of the AuthModel class in the variable named $auth.
Then if the user is logged in and wanting to log out, we call the let_out() method, which will perform the logging out itself.Then comes somehow the most important call of our index file, the call to the router's route() method.
This is exactly where we go deeper in trying to understand the core mechanisms that will make a module load upon being requested via the URL, be it a module to handle the serving of content, a module to allow the user to sign in or a module to manage some options in the back-end, it all loads from one object only - the router object.
In fact, back to design patterns, the very pattern that we will encounter now is called the factory pattern.
The factory pattern maybe be defined as a pattern in which the factory object creates other objects. This has a number of obvious benefits, such as the centralizing of the process of creating new objects. Therefore, if you need to change anything to the way you create and instantiate your new objects, then you only have to do so inside the factory.
In our case, the factory is our router. Our router, based on automatic URL parsing, will programmatically instantiate the controller of the module the client/user has asked for, and set it to work it by calling its initialize() method.
Just like with a regular factory. The client company needs a product, sends an order form, the order being our URL, the marketing people take the order form and tear down only the relevant page where the needed product is described, and handle it to the main plant people which in turn create the product and initialize it.
The __construct() method of our router class would be the order handling department, and the route method() would be the product building plant, in which is passed, as a parameter, the registry object we have created earlier, to be passed on, as a parameter again, to the controllers while being instantiated.
The registry works much as a stocking unit or a phone directory if you prefer, in the sense that you can store objects in it, to be later pulled out whenever required. How is that done? by using magic set() and get() methods, that will be triggered internally every time we will try to write/read to/from inaccessible properties, instead of having PHP generate an error.
<?php
// application/Registry.php
class Registry{
private $vars = array(); //we declare the vars array as private
public function __set($index, $value){ we declare the magic __set() method, triggered when trying to write inaccessable properties
$this->vars[$index] = $value; // we store in the array the index associated with its value
}
public function __get($index){ we declare the magic __get() method, triggered when trying to access inaccessable properties
return $this->vars[$index]; //we return the value of a given index contained in the array
}
}
?>
In our __set() method, which is run when writing data to inaccessible properties and which accepts an index/value pair, we simply store the index in the private array named $vars, which is the stocking unit or phone directory really, and we store its value altogether.In the __get() method, which is run when accessing data to from inaccessible properties, we simply return the value of a given array vars index by passing it as a parameter.
But back to our Router class
<?php
class Router{
private $path, $controller, $action; //declaring the private properties we need
public function __construct(){ //initializing our router object for when instantiated
$exp_menu_type_list = explode(";", MENU_TYPE_LIST); //we fetch and explode our menu type list again, we will need it here too
if(isset($_GET[RLINK])){ //if a module name is set in the URL
if( CLEAN_URLS == true // if clean URLs are on and google search is active and in two-column view mode
&& GOOGLE_CSE_PAGING_TYPE == 'two_column'
&& preg_match('/cx=(.*)&gs=(.*)&sa.x=(.*)&sa.y=(.*)/', $_SERVER['REQUEST_URI']))
{$request = 'search';} // we set our current module name as search, wich we store in the $request variable
else{ //otherwise
// if the set module name can be traced in our existing module type list
if(in_array($_GET[RLINK], $exp_menu_type_list)){$request = $_GET[RLINK];} // we assign that name to $request
else{
// otherwise we use the DEFAULT_RLINK constant value
$request = DEFAULT_RLINK;
}
}
}
else{$request = DEFAULT_RLINK;} //// otherwise we use the DEFAULT_RLINK constant value
$this->controller = !empty($request) ? ucfirst($request) : 'Index'; // if $request is not empty, we assign its value (with the first letter made uppercase) to the controller property, otherwise, we assign it the value 'Index'
$this->action = 'initialize'; // we set the action property to 'initialize'
}
public function route($registry){ // our factory's building plant
require_once('application/BaseController.php'); we include our abstract controller class from which every created controller will extend
$file = 'application/controllers/' . $this->controller . 'Controller.php'; // we build the full controller's class path using the retrieve URL module variable retrieved in the __construct() method
if(is_readable($file)){ // if the controller class is readable,
include $file; //we do include it
$class = $this->controller . 'Controller'; //and set the $class variable
}
else{
include 'application/controllers/Error404Controller.php'; //otherwise we include the 404error class
$class = 'Error404Controller'; //and set the $class to that
}
$controller = new $class($registry); //whatever the included controller, we instantiate it, creating the $controller object, passing our $registry object as parameter, itself containing our template object.
if (is_callable(array($controller, $this->action))){ //if within the newly created $controller object there's an initialize() method (demanded by the baseController) that's callable
$action = $this->action;
}
$controller->$action(); we do invoke that initialize() method we have called above
}
}
We first need to pay attention to its __constructor() method, in which we retrieve our URL module variable ($_GET[RLINK]) (the page we tore down from the order form), assign it, after checking it corresponds to a valid module name, to the $controller property, otherwise we assign it the value 'Index'. If google CSE is active in two-column view mode and with clean URLs on, we assign 'search' to the $controller property.Now onto the route() method, we bring our last core class, the base class, by including an abstract baseController class inside the router. The soon-to-be instantiated controller class will extend that BaseController class, so that it will inherit from its protected registry property, however, we as programmers will be free to decide how we want to implement the initialize() method that baseController is imposing on us. Note that as an abstract class, BaseController cannot be instantiated.
<?php
// application/BaseController.php
abstract class BaseController{
protected $registry; //holds a protected reference of the registry
function __construct($registry){
$this->registry = $registry; //sets the registry
}
abstract function initialize(); //forces each sub-class to have an initialize() method
}
?>
But back to the route() method, if the router then finds our controller file to be readable, it includes it, otherwise it includes the Error404Controller class.Whatever it has included, it instantiates. So now we have a controller object ready.
Further on, if that controller object's initialize() method is found callable, our router will call it. The initialize() method is where we will define the actions we want each controller to execute for itself and have it write some properties to the registry's template object to be passed directly to the corresponding view, in order to be used there. Within that method, we will also have each controller call the assign_theme() method of the registry's template object it holds.
This is how our user input is being routed. But then, we have to find and instantiate the model that corresponds to each created controller too. Because the models are the essential component that will provide our application with the business logic and data it needs. So, this is where the magic __autoload() function comes handy
function __autoload($class_name){
//The $class_name parameter is internal to PHP, so we don't have to set it ourselves.
//In fact, this will be set for us when the controller will first call the model it needs but the model will still not be included though.
//That will trigger the __autoload() function, in which we specify where to look to find the needed model and include it.
try{
$filename = $class_name.'.php';
$file = 'application/models/' . $filename; //but we make sure the __autoload() function performs its search within our model directory only
if (file_exists($file)){include ($file);} // if the class can be found, where we have told the __autoload() function to look for, it includes it
else { //otherswise throws and expception
throw new Exception('<p> </p><p> </p><div class="loading_model_fails">'.$GLOBALS['t_the_model_named'].' <u>' . $class_name . '</u> '.$GLOBALS['t_couldnt_be_found'].'</div>');
// note the use of globals for handling the translations, we will come back to that in the next settlements
}
}
catch(Exception $e){
echo $e->getMessage();
exit(0);
}
}
Did I say magic? Yes, it is a magic function, just as __set() and __get() are too. It will be called each time a called model class will not be recognized. So, it really is magic because, we do not know in advance which controller is going to be triggered. So, instead of having to include all our model classes, we only need to let PHP do the work of 'not recognizing' the model class the then-working controller object is trying to instantiate, call and instantiate it for us. Awesome, isn't it?But what of the view then? As we said earlier, the view files, which are in fact basic php files acting as templates, will be included by the template object's assign_theme() method, itself called in the controller's initialize() method.
<?php
// application/Template.php
class Template{
private $vars = array(); //declaring the array that will store the variables to be passed to the view
public function __set($index, $value){
//magically assigning index/pairs to that array
$this->vars[$index] = $value;
}
public function assign_theme(){
//the method the controller calls once it has finished
//interacting with the model and wants to display the view
try{
//We loop thru the vars array containing the variables the __set() method above has stored
foreach ($this->vars as $key => $value){
$$key = $value;
//we turn each key name to a variable using the $$ syntax, assigning it the same value it already held before entering the loop.
}
$common_template_files = array('header','main','footer'); // in reality, we do not display only one php view file per/module,
//as for obvious abstraction or visual design reasons, we choose to arbitrarily include the header, main and footer php view file,
//and we will include [module_name].php view file only from within the main view file,
//if the module_name variable has been sent by the controller
foreach($common_template_files as $ctf){ //for each common tpl files
$file = PATH_TO_THEMES.'/'.CURRENT_THEME.'/'.$ctf.'.php';
//we build its full path
if (!file_exists($file)){ //if the common tpl file does not exist
throw new Exception('<p> </p><p> </p><div class="loading_tpl_fails"><u>'.$ctf.'</u> '.$GLOBALS['t_couldnt_be_found'].'</div>');
// we throw an exception
}
else{
include($file); //but if it does exist, we include it
}
}
}
catch(Exception $e){
echo $e->getMessage();
exit(0);
}
}
}
?>
Note from the code above that we don't actually display only one php view file per/module, as for obvious abstraction or visual design reasons, we choose to arbitrarily include the header, main and footer view file instead and we will include the [module_name].php view file only from within the main view file called main.php, if the module_name variable has been set and sent by the controller.So here, in essence, we already have a full-blown MVC application in our hands. A router (factory) that loads the needed controller based on user input, a magically loaded model to provide, through its own set of properties and methods, for all the logic and data our application requires and a view file, fed by the template engine, itself containing a magic set() method and being contained in the registry object that was passed on to the controller as a parameter while being instantiated and that includes, at the controller's request, all the views needed for display, plus, that also stores, by means of set() methods, and displays all the data the controller wishes to handle to the view.
In the next settlement, we will set the focus on the controller, the model and the view, on how they are built, work together and we will come full circle by creating our first complete module, the page module. Do not miss it!
This article first appeared on Tuesday the 28th of October 2014 on RolandC.net.