Start your own MVC Framework with PHP

Start your own MVC Framework with PHP
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.


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
If you enjoy this article, please leave a like or a plus and let me know your thoughts in the comments.

Update: I have updated the MySqlDataAdapter.class.php in the sample code which replaced mysql_* functions to PDO (PHP Data Objects).


19 comments :

  1. 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.

    ReplyDelete
  2. Nice code for the most part. I understand that you followed a lot of what Anant
    Garg 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.

    ReplyDelete
  3. Hi,

    Thank 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

    ReplyDelete
  4. 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

    ReplyDelete
  5. Hello, Sandip,

    Thanks for your comment. Do other pages work or just blog? Do you place all folders under www/or www/mvc folder?

    Cheers,
    Elvis

    ReplyDelete
  6. Hi, Elvis.


    I 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

    ReplyDelete
  7. Hi, Donny,


    The 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

    ReplyDelete
  8. How would I go about adding/building a custom route?

    For 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! :)

    ReplyDelete
  9. Hi, Mike,

    Have 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

    ReplyDelete
  10. Hi,
    thank 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

    ReplyDelete
  11. Hi, thanks for your tutorial..,i downloaded and it run succesfull. but can you teach me how to use ajax request in this framework??

    ReplyDelete
  12. Good article, thanks for sharing this amazing stuff on PHP.
    PHP training course

    ReplyDelete
  13. Um, it would be the same way to request each page

    ReplyDelete
  14. Hi, thanks for your tutorial, I am thinking of applying this structure in different project, what changes i may have to make.

    ReplyDelete
  15. Hari Hara SudhanTuesday, May 19, 2015

    hi, 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()
    what can i do?

    ReplyDelete
  16. why we have to register the globals?

    ReplyDelete
  17. If you add read method in your model, it should be fine :)

    ReplyDelete
  18. Hi 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