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.
-
-
-
-
- index.php
- detail.php
- create.php
- update.php
- delete.php
-
- index.php
- .htaccess
-
- config.php
- .htaccess
-
-
- MyHelper.class.php
- MySqlDataAdapter.class.php
- bootstrap.php
- .htaccess
-
- .htaccess
-
Apache mode_rewrite module
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.htdocs/.htaccess
Options +FollowSymLinks <ifmodule mod_rewrite.c=""> # Tell PHP that the mod_rewrite module is ENABLED. SetEnv HTTP_MOD_REWRITE On RewriteEngine on RewriteRule ^$ public/ [L] RewriteRule (.*) public/$1 [L] </ifmodule>
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.
/public/.htaccess
Options +FollowSymLinks <ifmodule mod_rewrite.c=""> # Tell PHP that the mod_rewrite module is ENABLED. SetEnv HTTP_MOD_REWRITE On RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?_route=$1?%{QUERY_STRING} [PT,L] </ifmodule>
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.
/public/index.php
<?php // Ensure we have session if(session_id() === ""){ session_start(); } // define the directory separator define('DS', DIRECTORY_SEPARATOR); // define the application path define('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 dispatch require_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 use spl_autoload_register instead of __autoload (Please refer this discussion)
- 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.
/library/bootstrap.php
<?php // Ensure we have session if(session_id() === ""){ session_start(); } // the config file path $path = ROOT . DS . 'config' . DS . 'config.php'; // include the config settings require_once ($path); // Autoload any classes that are required spl_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 directory if(!$valid){ $valid = file_exists($classFile = $rootPath . 'library' . DS . 'core' . DS . $className . '.class.php'); } // if we cannot find any, then find library/mvc directory if(!$valid){ $valid = file_exists($classFile = $rootPath . 'library' . DS . 'mvc' . DS . $className . '.class.php'); } // if we cannot find any, then find application/controllers directory if(!$valid){ $valid = file_exists($classFile = $rootPath . 'application' . DS . 'controllers' . DS . $className . '.php'); } // if we cannot find any, then find application/models directory if(!$valid){ $valid = file_exists($classFile = $rootPath . 'application' . DS . 'models' . DS . $className . '.php'); } // if we have valid fild, then include it if($valid){ require_once($classFile); }else{ /* Error Generation Code Here */ } }); // remove the magic quotes MyHelpers::removeMagicQuotes(); // unregister globals MyHelpers::unregisterGlobals(); // register route $router = new Router($_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.php session_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.
/library/mvc/Router.class.php
<?php class Router { protected $_controller, $_action, $_view, $_params, $_route; public function __construct($_route){ $this->_route = $_route; $this->_controller = 'Controller'; $this->_action = 'index'; $this->_params = array(); $this->_view = false; // the initial view } private function parseRoute(){ $id = false; // parse path info if (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 route if (empty($path)){ $this->_controller = 'index'; $this->_action = 'index'; } else if (preg_match($cai, $path, $matches)){ $this->_controller = $matches[1]; $this->_action = $matches[2]; $id = $matches[3]; } else if (preg_match($ci, $path, $matches)){ $this->_controller = $matches[1]; $id = $matches[2]; } else if (preg_match($ca, $path, $matches)){ $this->_controller = $matches[1]; $this->_action = $matches[2]; } else if (preg_match($c, $path, $matches)){ $this->_controller = $matches[1]; $this->_action = 'index'; } else if (preg_match($i, $path, $matches)){ $id = $matches[1]; } // get query string from url $query = array(); $parse = parse_url($path); // if we have query string if(!empty($parse['query'])){ // parse query string parse_str($parse['query'], $query); // if query paramater is parsed if(!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 params unset($_GET['_route']); // merege the params $this->_params = array_merge($this->_params, $_GET); break; case "POST": // create case "PUT": // update case "DELETE": // delete { // ignore the file upload if(!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 not parse_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 have if(!empty($id)){ $this->_params['id']=$id; } if($this->_controller == 'index'){ $this->_params = array($this->_params); } } public function dispatch() { // 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 = new ReflectionClass($this->_controller); $m = $hasActionFunction ? $this->_action : 'defaultAction'; $f = $c->getMethod($m); $p = $f->getParameters(); $params_new = array(); $params_old = $this->_params; // re-map the parameters for($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 out if($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 404 unknownAction 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.
/library/mvc/Controller.class.php
<?php class Controller { protected $_model, $_controller, $_action; public $cfg, $view, $table, $id, $db, $userValidation; public function __construct($model="Model", $controller="Controler", $action="index") { // register configurations from config.php global $cfg; // set config $this->cfg = $cfg; // construct MVC $this->_controller = $controller; $this->_action = $action; // initialise the template class $this->view = new Template($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 */ protected function init(){ /* Put your code here*/ } /** * Redirect to action */ public function redirectToAction($action, $controller = false, $params = array()){ if($controller === false){ $controller = get_called_class(); }else if(is_string($controller) && class_exists($controller.'Controller')){ $controller = $controller.'Controller'; $controller = new $controller(); } return call_user_func_array(array($controller, $action), $params); } /** * process default action view */ public function defaultAction($params = null){ // make the default action path $path = MyHelpers::UrlContent("~/views/{$this->_controller}/{$this->_action}.php"); // if we have action name if(file_exists($path)){ $this->view->viewPath = $path; }else{ $this->unknownAction(); } // if we have parameters if(!empty($params) && is_array($params)){ // assign local variables foreach($params as $key=>$value){ $this->view->set($key, $value); } } // dispatch the result return $this->view(); } /** * unknownAction */ public function unknownAction($params = array()){ // feed 404 header to the client header("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 it if(file_exists($path)){ $this->view->viewPath = $path; return $this->view(); }else{ exit; //Do not do any more work in this script. } } /** * set the variables */ public function set($name,$value) { // set the parameters to the template class $this->view->set($name, $value); } /** * Returns the template result */ public function view(){ // dispatch the result of the template class return $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./library/mvc/Model.class.php
<?php class Model{ protected $_model; public $db, $controller; /** * Constructor for Model * */ public function __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(); } protected function init(){ /* 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.
At the end, we override the __toString() method in order for Router.class.php to dispatch the output.
/library/mvc/Template.class.php
<?php class Template { protected $_variables = array(), $_controller, $_action, $_bodyContent; public $viewPath, $section = array(), $layout; public function __construct($controller, $action) { $this->_controller = $controller; $this->_action = $action; // we set the configuration variables to local variables for rendering global $cfg; $this->set('cfg',$cfg); } /** * Set Variables */ public function set($name, $value) { $this->_variables[$name] = $value; } /** * set action */ public function setAction($action){ $this->_action = $action; } /** * RenderBody */ public function renderBody(){ // if we have content, then deliver it if(!empty($this->_bodyContent)){ echo $this->_bodyContent; } } /** * RenderSection */ public function renderSection($section){ if(!empty($this->section) && array_key_exists($section, $this->section)){ echo $this->section[$section]; } } /** * Display Template */ public function render() { // extract the variables for view pages extract($this->_variables); // the view path $path = MyHelpers::UrlContent('~/views/'); // start buffering ob_start(); // render page content if(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 buffer ob_end_clean(); // check if we have any layout defined if(!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 template include($this->layout); }else{ ob_start('MyHelpers::minify_content_js'); // just output the content echo $this->_bodyContent; } // end buffer ob_end_flush(); } /** * return the renderred html string */ public function __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./application/view/shared/_defaultLayout.php
<html> <head> <?php $this->renderSection('head');?> </head> <body> <?php $this->renderBody();?> </body> </html>
When you put your content of the page, you can also place sections to defined place in the _defaultLayout.php
/application/view/index/index.php
<?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.
Download Sample Project |
Update: I have updated the MySqlDataAdapter.class.php in the sample code which replaced mysql_* functions to PDO (PHP Data Objects).
PHP is a great way to start with career, but what many service providers do they Ref - hire asp.net developer as the ASP field gives more secure web applications, but smart and simple is the PHP.
ReplyDeleteNice code for the most part. I understand that you followed a lot of what Anant
ReplyDeleteGarg did in his article. Unfortunately that article is from 2009 and the mysql_ api that he and you both used to write your sql queries is deprecated and has been deprecated for some time now. It's going to be removed in future updates if it hasn't been already.
Hi,
ReplyDeleteThank you for your valuable comment. I will be changing the MySqlDataAdapter class from mysql_* to PDO in order to support PHP 5.1+
Cheers,
Elvis
i am not able to run this sample. i extract the project mvc, copy and paste on www folder and try to run . it goes to public folder and a page is displayed, but when click on blog link it displays The requested URL /mvc/blog was not found on this server. is there any setting
ReplyDeleteHello, Sandip,
ReplyDeleteThanks for your comment. Do other pages work or just blog? Do you place all folders under www/or www/mvc folder?
Cheers,
Elvis
Hi, Elvis.
ReplyDeleteI was downloaded the sample project too. And place all folders (extract it) under www. Running index (localhost/mvc) was worked fine, but when I click blog hyperlink (localhost/mvc/blog), i am not able to get blog page.
And I try to var_dump $_route in www/mvc/public/index.php and got this result -->> string(38) "redirect:/teras-sosis/public/blog.sql?"
Any suggestion?
Thanks before.
--Dony
Hi, Donny,
ReplyDeleteThe blog.sql is the sql script to create a table for mysql server. The mvc/blog connects to mysql database to perform the model operations. You may need to configure your mysql server when you run the sample.
Cheers,
Elvis
How would I go about adding/building a custom route?
ReplyDeleteFor example, instead of having "www.mysite.com/controller/action/1" (1 being an ID from a database table), it would be "www.mysite"com/controller/action/name" (the name column from the database table).
Please reply back when you can. Thanks! :)
Hi, Mike,
ReplyDeleteHave a look at the Router.class.php.
If you change the following from
$cai = '/^([\w]+)\/([\w]+)\/([\d]+).*$/';
to
$cai = '/^([\w]+)\/([\w]+)\/([\w]+).*$/';
it should work. But I would recommend you to add another condition in case you need numeric id :)
Elvis
Hi,
ReplyDeletethank for the tutorial.
I have downloaded your framework and I saw that you use minify in order to speed up the internet page load, isn't it ?
I have a question, reading the minify documentation, if I have high traffic I shoul use APC/Memcache adapters and I saw that you include it in your framework, could explain me how use it ?
Look forward your reply.
Regards
Andrea
Hi, thanks for your tutorial..,i downloaded and it run succesfull. but can you teach me how to use ajax request in this framework??
ReplyDeleteGood article, thanks for sharing this amazing stuff on PHP.
ReplyDeletePHP training course
Um, it would be the same way to request each page
ReplyDeleteHi, thanks for your tutorial, I am thinking of applying this structure in different project, what changes i may have to make.
ReplyDeletehi, really a good things for biggner..this is easy and really very easy top understand i created another controller, but i got some error.Call to undefined method Model::read()
ReplyDeletewhat can i do?
why we have to register the globals?
ReplyDeleteit is used for template
ReplyDeleteIf you add read method in your model, it should be fine :)
ReplyDeleteHi Elvis, really it is a great tutorial, thanks a lot for adding the sample. right now am stuck with a controller because the secuence is this: controller/method/params, I do require to have the url for only this controller like this: news/single-history which I think it become, controller/param. So I don't know how to make the in between method a wildcard or remove that space to acomplish root.local/news/one-news. Any help is very appreciated
ReplyDelete