Build a PHP MVC Framework in One Hour.

The purpose of this article is to show you how to build the struts for a PHP Model-View-Controller (MVC) Framework in about an hour of your time.

Contents

  1. The 10,000 Foot View
  2. Why Roll Your Own?
  3. Some Considerations
  4. Learning Curve
  5. The Layout
  6. OK Why?
  7. Getting Started
  8. Setting up the Front Controller
  9. Setting up the Router
  10. Create a Controller
  11. Create a Model
  12. Creating the View
  13. Download Source Files
  14. Room for Improvement
  15. Suggested Reading

The 10,000 Foot View

The purpose of a framework is to give you a basis for all future code within a project so that you can reuse tried-and-true patterns and methods over and again. Think of it in literal terms, where you have the foundation and frame of a house. If you can take those two things for granted always, then you can feel free to add wiring, put up dry-wall, roofing, add windows and doors and carpet and tile and what have you until you have a totally customize dream home. The only difference, is that you can use your foundation and frame like a cookie cutter for a dozen other homes without having to re-pour the foundation. And if you do things right, you might even be able to turn your carpet and wall paper into easy to add modules down the road. In a nutshell, that's a framework. There is a lot more to it than this, but understanding these basic tenets will get you started.

Why Roll Your Own?

For every language out there, there is a multitude of corresponding frameworks that have been written, tested, extended, and proven. Why then, would you want to roll your own? Maybe you haven't found a framework that suits you. Maybe you've got a vault of code in the treasure chest that you have been saving up, and just wish you could take the time to organize it so that is reusable in future projects. Maybe you are bored or fall into a mile-long list of other reasons that would persuade you to craft your own.

Some Considerations

We're going to show you how to roll your own framework, using the Model, View, Controller pattern (more on that later, or click here for wikipedia gratification). Because we are going to do this in such a short amount of time, you must understand the following:

Learning Curve

We are going to assume the following:

The Layout

Since we are going to use the architectural pattern Model-View-Controller (MVC), we are going to start with three folders and an index.php file in the root directory of your site. The purpose of MVC (if you did your wikipedia reading you should be on par), is to separate the program logic (model) from the user interface (view). The controller acts as an intermediary between the view and the model. A user interacts with the view, commands are passed to the controller, who then acts with the model to retrieve information (like database entries, etc.) Once the controller and the model have the result for the view's request, the controller returns it to the view, and this continues until the interaction is complete. Since web pages are what we call stateless transacations, this generally happens when a user selects an action on a web page (view) and then sees the corresponding result as generated by the controller and model interaction. This is a very standard concept in the software industry, and deep down this article barely scratches the surface, but acts as a nice starting point for getting your feet wet.

OK Why?

As you may be seeing already, organizing your code in this fashion makes it very simple to separate the reusable logic from the often specific user interface. Changes to the user's interaction can easily be made without messing with the back-end logic. Code that is very repetitive, such as pulling a username and password from a database to check the login state of a user can be put into a reusable module, that once written, never has to be rewritten until you decide to change how it works. If all of this code were in the user interface document, every time you needed a new user interface, you would have to copy and paste the entire document, tweak the parts that broke once moved to another location, alter the login, debug, test, etc. This tends to get very burdensome over time.

At, that, let's get away from theory, as it is better written by many individuals out in the wide world. If you need/want to learn more about these conceptes, check out our suggested reading and that should get you pointed toward some excellent books on the topic.

Getting Started

In your web root, create the following folders

models
views
controllers

Now, in the web root, create a file called:

index.php

Your application hierarchy should now look soemthing like this (depending on your sort method):

-WEB_ROOT_FOLDER
index.php
-models
-views
-controllers

The index.php is going to be the Front Controller of your application, meaning that all page requests will go through it. We are going to use some code to route all user requests to the appropriate files in the controllers folder. When we are done, your users will navigate pages using links like the following:

http://yoursite.com/index.php?page1
http://yoursite.com/index.php?page2
http://yoursite.com/index.php?page3

More on how this is going to work in a bit.

For the remainder of this article, we are going to assume the following server layout:

web root: /var/www/
domain name: http://www.yourdomain.com

if your paths are different, you will need to adjust them accordingly throughout the examples. Luckily, you should only have to change them in one location! Hey this framework stuff is working out already!

Setting up the Front Controller

In index.php we are going to define our web root and domain name so they are available to the whole application:

toggle plain-text
  1. <?php
  2.  
  3. //WEB_ROOT_FOLDER is the name of the parent folder you created these documents in.
  4. define('SERVER_ROOT' , '/var/www/WEB_ROOT_FOLDER');
  5.  
  6. //yoursite.com is your webserver
  7. define('SITE_ROOT' , 'http://yoursite.com');
  8.  
  9. ?>

The reason we need both of these, is because internally your application needs to be able to include files relative to its root directory, and externally, your images, javascript, links, videos, etc. need to be able to intertwine. Since index.php is going to be the starting point for the entire application, by defining these values here, they will be available to every subsequent file!

Now let's break away from the index for a moment, but keep it open and nearby, as we will be revisiting it soon!

Setting up the Router

In the controllers folder you just created, let's create a file called router.php. This file is going to be the handler for all web page requests. Think of it like you think of your wireless router in your house, it takes connections from the cable or satellite modem, and routes internet to every computer in the house. Your router.php is going to take web page requests passed to index.php and route the request to different files (controllers) in your application.

in router.php add the following code:

toggle plain-text
  1. <?php
  2.  
  3. //fetch the passed request
  4. $request = $_SERVER['QUERY_STRING'];
  5.  
  6. ?>
  7.  

this is going to grab the request string passed to the application. The request string is everything following the '?' in the URL. Using the examples from earlier:

http://yoursite.com/index.php?page1

will yield page1 in the request string.

let's make this happen by adding the following lines to router.php:

toggle plain-text
  1. //parse the page request and other GET variables
  2. $parsed = explode('&' , $request);
  3.  
  4. //the page is the first element
  5. $page = array_shift($parsed);
  6.  
  7. //the rest of the array are get statements, parse them out.
  8. $getVars = array();
  9. foreach ($parsed as $argument)
  10. {
  11.     //split GET vars along '=' symbol to separate variable, values
  12.     list($variable , $value) = split('=' , $argument);
  13.     $getVars[$variable] = $value;
  14. }
  15.  
  16. //this is a test , and we will be removing it later
  17. print "The page your requested is '$page'";
  18. print '<br/>';
  19. $vars = print_r($getVars, TRUE);
  20. print "The following GET vars were passed to the page:<pre>".$vars."</pre>";
  21.  

Now we need to include the router from index.php, so update index.php to look like:

toggle plain-text
  1. <?php
  2. /**
  3.  * Define document paths
  4.  */
  5. define('SERVER_ROOT' , '/var/www/mvc');
  6. define('SITE_ROOT' , 'http://localhost');
  7.  
  8. /**
  9.  * Fetch the router
  10.  */
  11. require_once(SERVER_ROOT . '/controllers/' . 'router.php');
  12.  

If everything is going smoothly, you should be able to open up your browser and enter the following:

http://yoursite.com/index.php?news&article=howtobuildaframework

you should see the following output:

	The page your requested is 'news'
	The following GET vars were passed to the page:

	Array
	(
		[article] => howtobuildaframework
	)

If you don't, check the steps above, make sure your web server is configured appropriately, check syntax, etc.

Now, let's add a page to our website. Once that is done, we will tweak the router.php to serve the page rather than just print the message above.

Create a Controller

Create a document in controllers called news.php and add the following class:

toggle plain-text
  1. <?php
  2. /**
  3.  * This file handles the retrieval and serving of news articles
  4.  */
  5. class News_Controller
  6. {
  7.     /**
  8.      * This template variable will hold the 'view' portion of our MVC for this
  9.      * controller
  10.      */
  11.     public $template = 'news';
  12.  
  13.     /**
  14.      * This is the default function that will be called by router.php
  15.      *
  16.      * @param array $getVars the GET variables posted to index.php
  17.      */
  18.     public function main(array $getVars)
  19.     {
  20.         //this is a test , and we will be removing it later
  21.         print "We are in news!";
  22.         print '<br/>';
  23.         $vars = print_r($getVars, TRUE);
  24.         print
  25.         (
  26.             "The following GET vars were passed to this controller:" .
  27.             "<pre>".$vars."</pre>"
  28.         );
  29.     }
  30. }
  31.  

Notice we copied and modified the test code from router.php and placed it with some modifications in our default main() function in news.php. Let's strip that code out of router.php and make our code look like this:

toggle plain-text
  1. <?php
  2. /**
  3.  * This controller routes all incoming requests to the appropriate controller
  4.  */
  5.  
  6. //fetch the passed request
  7. $request = $_SERVER['QUERY_STRING'];
  8.  
  9. //parse the page request and other GET variables
  10. $parsed = explode('&' , $request);
  11.  
  12. //the page is the first element
  13. $page = array_shift($parsed);
  14.  
  15. //the rest of the array are get statements, parse them out.
  16. $getVars = array();
  17. foreach ($parsed as $argument)
  18. {
  19.     //split GET vars along '=' symbol to separate variable, values
  20.     list($variable , $value) = split('=' , $argument);
  21.     $getVars[$variable] = $value;
  22. }
  23.  
  24. //compute the path to the file
  25. $target = SERVER_ROOT . '/controllers/' . $page . '.php';
  26.  
  27. //get target
  28. if (file_exists($target))
  29. {
  30.     include_once($target);
  31.  
  32.     //modify page to fit naming convention
  33.     $class = ucfirst($page) . '_Controller';
  34.  
  35.     //instantiate the appropriate class
  36.     if (class_exists($class))
  37.     {
  38.         $controller = new $class;
  39.     }
  40.     else
  41.     {
  42.         //did we name our class correctly?
  43.         die('class does not exist!');
  44.     }
  45. }
  46. else
  47. {
  48.     //can't find the file in 'controllers'!
  49.     die('page does not exist!');
  50. }
  51.  
  52. //once we have the controller instantiated, execute the default function
  53. //pass any GET varaibles to the main method
  54. $controller->main($getVars);
  55.  

Be sure to include the comment added to the top of the script. I have added it now, because the file now has that functionality. If all goes well, you should be able to call the same URL as before and see the message now printed out from the News_Controller. Notice that we used die() statements to handle errors. Later on, we can replace these with better error handling methods, but for now these will work. Try using one of the other URLs from earlier to throw a 'page does not exist!' error.

Create a Model

Let's make the News_Controller do a bit more. Let's presume we have a small selection of news snippets that we would like to serve to the user. It should be the job of the News_Controller to call a model to fetch the appropriate news snippets, whether they be stored in a database, a flat file, etc. In the models folder create a new file called 'news.php'. And add the following code:

toggle plain-text
  1. <?php
  2. /**
  3.  * The News Model does the back-end heavy lifting for the News Controller
  4.  */
  5. class News_Model
  6. {
  7.     public function __construct()
  8.     {
  9.         print 'I am the news model';
  10.     }
  11. }
  12.  

For now, we've got a simple test that will print when the model is instantiated. Now let's load the model. Modify the News_Controller main() function to reflect the following:

toggle plain-text
  1. public function main(array $getVars)
  2. {
  3.     $newsModel = new News_Model;
  4. }
  5.  

Now reload the page and you should see:

	Fatal error: Class 'News_Model' not found in /var/www/mvc/controllers/news.php on line xx

Wait a minute, that's not what we want! What is happening here, is we are trying to load a class that does not exist. The reason it does not exist is because we haven't yet loaded the file containing it, /models/news.php.

Let's take a minute to revisit the router.php file to add a bit of code to the top of the file:

toggle plain-text
  1. //Automatically includes files containing classes that are called
  2. function __autoload($className)
  3. {
  4.     //parse out filename where class should be located
  5.     list($filename , $suffix) = split('_' , $className);
  6.  
  7.     //compose file name
  8.     $file = SERVER_ROOT . '/models/' . strtolower($filename) . '.php';
  9.  
  10.     //fetch file
  11.     if (file_exists($file))
  12.     {
  13.         //get file
  14.         include_once($file);       
  15.     }
  16.     else
  17.     {
  18.         //file does not exist!
  19.         die("File '$filename' containing class '$className' not found.");  
  20.     }
  21. }
  22.  

This function 'overloads' the autoload functionality built into PHP. Basically, this 'magic function' allows us to intercept the action that PHP takes when we try to instantiate a class that does not exist. By using the __autoload function in our router, we can tell PHP where to find the file containing the class we are looking for. Assuming that you follow the class and file naming convention set forth in this article, everytime you need to instantiate a class, you can safely do so without having to manually include the file!

Save the changes to router.php and refresh your browser, now you should see:

	I am the news model

Let's create some functionality in the model to provide some articles. For now, we'll simply create a class array with some articles in it, and provide a function to allow the controller to pull an article by name.

Change the news model to the following:

toggle plain-text
  1. <?php
  2. /**
  3.  * The News Model does the back-end heavy lifting for the News Controller
  4.  */
  5. class News_Model
  6. {
  7.     /**
  8.      * Array of articles. Array keys are titles, array values are corresponding
  9.      * articles.
  10.      */
  11.     private $articles = array
  12.     (
  13.         //article 1
  14.         'new' => array
  15.         (
  16.             'title' => 'New Website' ,
  17.             'content' => 'Welcome to the site! We are glad to have you here.'
  18.         )
  19.         ,
  20.         //2
  21.         'mvc' => array
  22.         (
  23.             'title' => 'PHP MVC Frameworks are Awesome!' ,
  24.             'content' => 'It really is very easy. Take it from us!'
  25.         )
  26.         ,
  27.         //3
  28.         'test' => array
  29.         (
  30.             'title' => 'Testing' ,
  31.             'content' => 'This is just a measly test article.'
  32.         )
  33.     );
  34.  
  35.     public function __construct()
  36.     {
  37.     }
  38.  
  39.     /**
  40.      * Fetches article based on supplied name
  41.      *
  42.      * @param string $articleName
  43.      *
  44.      * @return array $article
  45.      */
  46.     public function get_article($articleName)
  47.     {
  48.         //fetch article from array
  49.         $article = $this->articles[$articleName];
  50.    
  51.         return $article;
  52.     }
  53.  
  54. }
  55.  

Now, in the news controller, update the main function to this:

toggle plain-text
  1. public function main(array $getVars)
  2. {
  3.     $newsModel = new News_Model;
  4.    
  5.     //get an article
  6.     $article = $newsModel->get_article('test');
  7.    
  8.     print_r($article);
  9. }
  10.  

Give yourself a pat on the back if you noticed the security error in passing the unfiltered GET variable to the model. Give yourself an extra pat if it made you cringe. Although it is extremely important to always clean incoming data, don't worry too much about this right now, just keep it in mind for later.

if you call the file like so:

http://yourdomain.com/mvc/index.php?news&article=test

you should see something along the lines of:

	Array ( [title] => Testing [content] => This is just a measly test article. ) 

Creating the View

Now that we have the model and controller functionality going, the last step is to get the views going. Remember that the view is your presentation layer. It is the portion of your application that users will be most familiar with. Earlier I mentioned that the purpose of the view is to provide a user interface that is separate from the logic. There are many ways to go about this. You can use a templating engine such as smarty (http://smarty.net) or something simliar. You could roll your own, but this can be a pretty daunting task. Lastly you could use PHP views.

For now, PHP views should work just fine. This harks back to the old HTML code with inline PHP statements. The only difference is that all of our logic is being kept out of the view. Consider the following code:

toggle plain-text
  1. <html>
  2.     <head></head>
  3.     <body>
  4.         <h1>Welcome to Our Website!</h1>
  5.         <hr/>
  6.         <h2>News</h2>
  7.         <h4><?=$data['title'];?></h4>
  8.         <p><?=$data['content'];?></p>
  9.     </body>
  10. </html>
  11.  

Notice the inline PHP tags utilizing the PHP shortcut operator. This will output our content directly into the HTML. Drop this code into a file in the views folder and name it 'news.php'.

Now that we have our view, we need a way to interact with it.

In the models folder create a file called view.php with the following:

toggle plain-text
  1. <?php
  2. /**
  3.  * Handles the view functionality of our MVC framework
  4.  */
  5. class View_Model
  6. {
  7.     /**
  8.      * Holds variables assigned to template
  9.      */
  10.     private $data = array();
  11.  
  12.     /**
  13.      * Holds render status of view.
  14.      */
  15.     private $render = FALSE;
  16.  
  17.     /**
  18.      * Accept a template to load
  19.      */
  20.     public function __construct($template)
  21.     {
  22.         //compose file name
  23.         $file = SERVER_ROOT . '/views/' . strtolower($template) . '.php';
  24.    
  25.         if (file_exists($file))
  26.         {
  27.             /**
  28.              * trigger render to include file when this model is destroyed
  29.              * if we render it now, we wouldn't be able to assign variables
  30.              * to the view!
  31.              */
  32.             $this->render = $file;
  33.         }      
  34.     }
  35.  
  36.     /**
  37.      * Receives assignments from controller and stores in local data array
  38.      *
  39.      * @param $variable
  40.      * @param $value
  41.      */
  42.     public function assign($variable , $value)
  43.     {
  44.         $this->data[$variable] = $value;
  45.     }
  46.  
  47.     public function __destruct()
  48.     {
  49.         //parse data variables into local variables, so that they render to the view
  50.         $data = $this->data;
  51.    
  52.         //render view
  53.         include($this->render);
  54.     }
  55. }
  56.  

This file model will handle the loading and generation of our view. It works by keeping variable assignments passed to it in the assign() function in a local data array. The supplied template's existence is checked in __construct and is loaded and rendered in __destruct. When the view is rendered, the local data array is pushed to the view so that the assigned variables are output to the user interface.

Now, the last thing to do is load the view from the News_Controller. Modify news.php in the controllers folder to reflect the following:

toggle plain-text
  1. <?php
  2. /**
  3.  * This file handles the retrieval and serving of news articles
  4.  */
  5. class News_Controller
  6. {
  7.     /**
  8.      * This template variable will hold the 'view' portion of our MVC for this
  9.      * controller
  10.      */
  11.     public $template = 'news';
  12.  
  13.     /**
  14.      * This is the default function that will be called by router.php
  15.      *
  16.      * @param array $getVars the GET variables posted to index.php
  17.      */
  18.     public function main(array $getVars)
  19.     {
  20.         $newsModel = new News_Model;
  21.    
  22.         //get an article
  23.         $article = $newsModel->get_article($getVars['article']);
  24.    
  25.         //create a new view and pass it our template
  26.         $view = new View_Model($this->template);
  27.    
  28.         //assign article data to view
  29.         $view->assign('title' , $article['title']);
  30.         $view->assign('content' , $article['content']);
  31.     }
  32. }
  33.  

When you load your page, you should see your HTML template with the values for title and content parsed in appropriately!

That's it, your basic MVC is complete!

Download Source Files

If you'd like to download the complete files used in this tutorial, choose from the following formats:

Room for Improvement

Since this is a very quick, basic MVC framework, there is certainly room for a great deal of improvement. Take your framework to the next step by trying the following:

Got this part down? Head on over to Part Two to tackle the next part of this tutorial!

};