I have been using ASP .NET MVC for many years. It is a complex framework but not difficult to learn (at least for .NET developers). Anyway, I like the way it handles templates and routing. Few years ago, one of my clients asked me to developed a simple ERP system with PHP and MySQL. My first thought was: "Why not developing a simple MVC framework with PHP?". It would allow me to utilise the backend code and avoid mixing code in classes.
I started to study many PHP MVC Frameworks such as Zend Framework, Symfony and CakePHP but I realised those "ready-to-use" frameworks are overkilled for this simple project and I would like to have codes that I could manipulate easily and apply them to next simple projects without making my brand hurts. Luckily, I found this article: Write your own PHP MVC Framework. It is a very good article and it explained the concept of MVC comprehensively. If you just start learning MVC, this is a great place start. Well, the truth is the article inspired me to create my own little MVC framework with PHP.
Anyway, before you continue reading, I recommend you to read through the article I mentioned above. My version of MVC is a little bit different from Anant Garg's but I believe you will find it useful.
In this article, I would like to focus on the routing and template operations. Since Anant Garg has covered most important parts of MVC, I will just point out some features of my own framework in this article.
Before we start talking about the details, let's have a look at the directory structure. You can expand the folder below and link to sample codes of this article. The structure is pretty much the same as Anant Garg's. The only difference is that I put all public assessable files (images, style sheets and js scripts) into public folder.
Routing plays an important role in MVC framework since it translate your URL to match your controllers and actions. When any Http request pass to your site, it will go to htdocs/.htaccess first. Then we parse the URL and redirect the request to htdocs/public folder. In fact, we only have a single entry point since every request will be going to htdocs/public folder and waiting for next process.
In the root of the public folder, we have another .htaccess waiting for us to process the requests (sample below). We put every public accessible files into the public folder, so it is much easier to organise them in the future. If the requests are images, videos, style sheets or script files, then we just feed them to clients directly. If the request is the actual page, then we parse the URL and let the /public/index.php to handle the rest of actions. The actual URL and query strings are assigned to _route variable.
Once we have an actual page request, then our MVC bootstrap process starts from here. The only difference here between Anant Garg's code is we need to get the original query strings since PHP parses the query string to be part of URL entity.
<?php// Ensure we have sessionif(session_id()===""){session_start();}// define the directory separatordefine('DS',DIRECTORY_SEPARATOR);// define the application pathdefine('ROOT',dirname(dirname(__FILE__)));// the routing url, we need to use original 'QUERY_STRING' from server paramater because php has parsed the url if we use $_GET$_route=isset($_GET['_route'])?preg_replace('/^_route=(.*)/','$1',$_SERVER['QUERY_STRING']):'';// start to dispatchrequire_once(ROOT.DS.'library'.DS.'bootstrap.php');
Bootstrap
Basically, this part does not have big differences between Anant Garg's concept. The differences are:
I included a config.php file to serve configuration parameters such as site name and title
I wrapped the "removeMagicQuotes" and "unregisterGlobals" functions to MyHelpers class
I use use the Router class to handle further operations and dispatch the outputs
I call session_write_close and the end of the operation to unlock the session data. This is useful when you have many concurrent connections such as using Ajax operations.
<?php// Ensure we have sessionif(session_id()===""){session_start();}// the config file path$path=ROOT.DS.'config'.DS.'config.php';// include the config settingsrequire_once($path);// Autoload any classes that are requiredspl_autoload_register(function($className){//$className = strtolower($className);$rootPath=ROOT.DS;$valid=false;// check root directory of library$valid=file_exists($classFile=$rootPath.'library'.DS.$className.'.class.php');// if we cannot find any, then find library/core directoryif(!$valid){$valid=file_exists($classFile=$rootPath.'library'.DS.'core'.DS.$className.'.class.php');}// if we cannot find any, then find library/mvc directoryif(!$valid){$valid=file_exists($classFile=$rootPath.'library'.DS.'mvc'.DS.$className.'.class.php');}// if we cannot find any, then find application/controllers directoryif(!$valid){$valid=file_exists($classFile=$rootPath.'application'.DS.'controllers'.DS.$className.'.php');}// if we cannot find any, then find application/models directoryif(!$valid){$valid=file_exists($classFile=$rootPath.'application'.DS.'models'.DS.$className.'.php');}// if we have valid fild, then include itif($valid){require_once($classFile);}else{/* Error Generation Code Here */}});// remove the magic quotesMyHelpers::removeMagicQuotes();// unregister globalsMyHelpers::unregisterGlobals();// register route$router=newRouter($_route);// finaly we dispatch the output$router->dispatch();// close session to speed up the concurrent connections// http://php.net/manual/en/function.session-write-close.phpsession_write_close();
The Router (Router.class.php)
The Router class plays an important role in our framework. It translates the controllers, actions for further process. When the Router::dispatch method is called, it starts parsing the _route value first. After we found what controller and action are, then we process the request and dispatch the results to the clients.
We uses regular expression match by preg_match method to parse our routing patterns. If you have other patterns, you can simply add more test patterns to the code (Line 25 - 29).
After we found out what controller and action are, we start to parse our query string. This step is necessary since PHP has parsed the query string to URL formated string (Line 54 - 68).
After we got our query string, then we need to determine which request method has client sent in order to retrieve our parameters. This part supports RESTful and standard Http requests (Line 71 - 104). Since I use RESTful data store from ExtJs a lot, therefore I included in the Router class.
The parameters may not include the "id", therefore we need to put "id" value to our parameters array.
We start to validate the existence of controller class, model class and action method. If all of them exist, then we try to match the action method's arguments with requested parameters. We then use call_user_func_array to call the action method with filtered parameters (Line 120 - 152).
The last action would be deliver the results to the clients which you can see at Line 155. The $this->_view value is actually the result from the Template.class.php.
<?phpclassRouter{protected$_controller,$_action,$_view,$_params,$_route;publicfunction__construct($_route){$this->_route=$_route;$this->_controller='Controller';$this->_action='index';$this->_params=array();$this->_view=false;// the initial view}privatefunctionparseRoute(){$id=false;// parse path infoif(isset($this->_route)){// the request path$path=$this->_route;// the rules to route$cai='/^([\w]+)\/([\w]+)\/([\d]+).*$/';// controller/action/id$ci='/^([\w]+)\/([\d]+).*$/';// controller/id$ca='/^([\w]+)\/([\w]+).*$/';// controller/action$c='/^([\w]+).*$/';// action$i='/^([\d]+).*$/';// id// initialize the matches$matches=array();// if this is home page routeif(empty($path)){$this->_controller='index';$this->_action='index';}elseif(preg_match($cai,$path,$matches)){$this->_controller=$matches[1];$this->_action=$matches[2];$id=$matches[3];}elseif(preg_match($ci,$path,$matches)){$this->_controller=$matches[1];$id=$matches[2];}elseif(preg_match($ca,$path,$matches)){$this->_controller=$matches[1];$this->_action=$matches[2];}elseif(preg_match($c,$path,$matches)){$this->_controller=$matches[1];$this->_action='index';}elseif(preg_match($i,$path,$matches)){$id=$matches[1];}// get query string from url $query=array();$parse=parse_url($path);// if we have query stringif(!empty($parse['query'])){// parse query stringparse_str($parse['query'],$query);// if query paramater is parsedif(!empty($query)){// merge the query parameters to $_GET variables$_GET=array_merge($_GET,$query);// merge the query parameters to $_REQUEST variables$_REQUEST=array_merge($_REQUEST,$query);}}}// gets the request method$method=$_SERVER["REQUEST_METHOD"];// assign params by methods switch($method){case"GET":// view// we need to remove _route in the $_GET paramsunset($_GET['_route']);// merege the params$this->_params=array_merge($this->_params,$_GET);break;case"POST":// createcase"PUT":// updatecase"DELETE":// delete{// ignore the file uploadif(!array_key_exists('HTTP_X_FILE_NAME',$_SERVER)){if($method=="POST"){$this->_params=array_merge($this->_params,$_POST);}else{// temp params $p=array();// the request payload$content=file_get_contents("php://input");// parse the content string to check we have [data] field or notparse_str($content,$p);// if we have data field$p=json_decode($content,true);// merge the data to existing params$this->_params=array_merge($this->_params,$p);}}}break;}// set param id to the id we haveif(!empty($id)){$this->_params['id']=$id;}if($this->_controller=='index'){$this->_params=array($this->_params);}}publicfunctiondispatch(){// call to parse routes$this->parseRoute();// set controller name$controllerName=$this->_controller;// set model name$model=$this->_controller.'Model';// if we have extended model$model=class_exists($model)?$model:'Model';// assign controller full name$this->_controller.='Controller';// if we have extended controller$this->_controller=class_exists($this->_controller)?$this->_controller:'Controller';// construct the controller class$dispatch=new$this->_controller($model,$controllerName,$this->_action);// if we have action function in controller$hasActionFunction=(int)method_exists($this->_controller,$this->_action);// we need to reference the parameters to a correct order in order to match the arguments order // of the calling function$c=newReflectionClass($this->_controller);$m=$hasActionFunction?$this->_action:'defaultAction';$f=$c->getMethod($m);$p=$f->getParameters();$params_new=array();$params_old=$this->_params;// re-map the parametersfor($i=0;$i<count($p);$i++){$key=$p[$i]->getName();if(array_key_exists($key,$params_old)){$params_new[$i]=$params_old[$key];unset($params_old[$key]);}}// after reorder, merge the leftovers$params_new=array_merge($params_new,$params_old);// call the action method$this->_view=call_user_func_array(array($dispatch,$m),$params_new);// finally, we print it outif($this->_view){echo$this->_view;}}}
The Controller (Controller.class.php)
Like many other MVC frameworks, our controller plays logical operations. At Line 18, we assign our configurations (From config.php) for internal use. Then we initialise the Template.class.php and models.
Well, I am a bit lazy, so I made a defaultAction method. When there is no available actions but we have outputs then we use defaultAction to call the related files in the view folder. If we cannot find any related files, then we throw a 404unknownAction for clients.
I just added few methods for my own, other than that it is pretty much the same as Anant Garg's controller.
Please note that I use MySqlDataAdapter.class.php as $db to perform database operations instead of putting it into the Model class. It is much easier for me to update the database wrapper class since I may use other type of database connections such as T-SQL or SQLite.
<?phpclassController{protected$_model,$_controller,$_action;public$cfg,$view,$table,$id,$db,$userValidation;publicfunction__construct($model="Model",$controller="Controler",$action="index"){// register configurations from config.phpglobal$cfg;// set config$this->cfg=$cfg;// construct MVC$this->_controller=$controller;$this->_action=$action;// initialise the template class$this->view=newTemplate($controller,$action);// call the function for derived class $this->init();// start contruct models$this->_model=new$model($this->db);$this->_model->controller=$this;$this->table=$controller;}/**
* Initialize the required classes and variables
*/protectedfunctioninit(){/* Put your code here*/}/**
* Redirect to action
*/publicfunctionredirectToAction($action,$controller=false,$params=array()){if($controller===false){$controller=get_called_class();}elseif(is_string($controller)&&class_exists($controller.'Controller')){$controller=$controller.'Controller';$controller=new$controller();}returncall_user_func_array(array($controller,$action),$params);}/**
* process default action view
*/publicfunctiondefaultAction($params=null){// make the default action path$path=MyHelpers::UrlContent("~/views/{$this->_controller}/{$this->_action}.php");// if we have action nameif(file_exists($path)){$this->view->viewPath=$path;}else{$this->unknownAction();}// if we have parametersif(!empty($params)&&is_array($params)){// assign local variablesforeach($paramsas$key=>$value){$this->view->set($key,$value);}}// dispatch the resultreturn$this->view();}/**
* unknownAction
*/publicfunctionunknownAction($params=array()){// feed 404 header to the clientheader("HTTP/1.0 404 Not Found");// find custom 404 page$path=MyHelpers::UrlContent("~/views/shared/_404.php");// if we have custom 404 page, then use itif(file_exists($path)){$this->view->viewPath=$path;return$this->view();}else{exit;//Do not do any more work in this script. }}/**
* set the variables
*/publicfunctionset($name,$value){// set the parameters to the template class$this->view->set($name,$value);}/**
* Returns the template result
*/publicfunctionview(){// dispatch the result of the template classreturn$this->view;}}
The Model (Model.class.php)
Well, since most of database operations are handled by MySqlDataAdapter.php, the Model class is just a base class to assist Controller class.
<?phpclassModel{protected$_model;public$db,$controller;/**
* Constructor for Model
*
*/publicfunction__construct($db){$this->db=$db;$this->_model=get_class($this);$defaultModel=($this->_model=='Model');if(!$defaultModel){$this->table=preg_replace('/Model$/','',$this->_model);// remove ending Model }$this->init();}protectedfunctioninit(){/* Put your code here*/}}
The Template (Template.class.php)
The Template class handles our output operations. I use ob_start, ob_get_contents, ob_end_clean and ob_end_flush to hold the output data and join them together before dispatching to clients.
You may also notice that I use PHP minify to reduce the size of the outputs. If the operation is html output then I minify Html, CSS and JavaScript contents. If the operation is Ajax or just JavaScript contents, then I only minify it with JS minify only.
<?phpclassTemplate{protected$_variables=array(),$_controller,$_action,$_bodyContent;public$viewPath,$section=array(),$layout;publicfunction__construct($controller,$action){$this->_controller=$controller;$this->_action=$action;// we set the configuration variables to local variables for renderingglobal$cfg;$this->set('cfg',$cfg);}/**
* Set Variables
*/publicfunctionset($name,$value){$this->_variables[$name]=$value;}/**
* set action
*/publicfunctionsetAction($action){$this->_action=$action;}/**
* RenderBody
*/publicfunctionrenderBody(){// if we have content, then deliver itif(!empty($this->_bodyContent)){echo$this->_bodyContent;}}/**
* RenderSection
*/publicfunctionrenderSection($section){if(!empty($this->section)&&array_key_exists($section,$this->section)){echo$this->section[$section];}}/**
* Display Template
*/publicfunctionrender(){// extract the variables for view pagesextract($this->_variables);// the view path$path=MyHelpers::UrlContent('~/views/');// start bufferingob_start();// render page contentif(empty($this->viewPath)){include($path.$this->_controller.DS.$this->_action.'.php');}else{include($this->viewPath);}// get the body contents$this->_bodyContent=ob_get_contents();// clean the bufferob_end_clean();// check if we have any layout definedif(!empty($this->layout)&&(!MyHelpers::isAjax())){// we need to check the path contains app prefix (~)$this->layout=MyHelpers::UrlContent($this->layout);// start buffer (minify pages)ob_start('MyHelpers::minify_content');// include the templateinclude($this->layout);}else{ob_start('MyHelpers::minify_content_js');// just output the contentecho$this->_bodyContent;}// end bufferob_end_flush();}/**
* return the renderred html string
*/publicfunction__toString(){$this->render();return'';}}
How to use template?
The logic is similar to .NET MVC's template operations. We have shared pages to accommodate each view page. Simply assign the main page layout at top of each view page ($this->layout="~/view/shared/_defaultLayout.php"). If we have dynamic sections, then we place $this->section['param'] at the view page.
<?php// The default layout template$this->layout='~/views/shared/_defaultLayout.php';// The value to put on the head section$this->section['head']="<script src='http://code.jquery.com/jquery-latest.min.js'></script>";?><strong>This is the text I want to show in the body</strong>
The output result
<html><head><script src='http://code.jquery.com/jquery-latest.min.js'></script></head><body><strong>This is the text I want to show in the body</strong></body></html>
In conclusion, the concept is not difficult and should be easy to apply on any simple project. If you understand how Anant Garg wants to achieve, you should be able to comprehend this post. You can download the sample code from HERE for further reading. The sample has more implementations on this simple MVC framework.
The standard paragraphs Welcome to this demo page! Here, you’ll get an exclusive preview of our cutting-edge platform designed to revolutionise your digital experience. Our...