Skip to content

March 26, 2010

2

Building a simple MVC framework

Introduction

I don’t know about you, but it took me a while to fully grasp the concepts of MVCs. I mean sure, it’s a Model View Controller framework and the model manages the raw data and links it to the database, the controller manages the requests to a specific module and is the translator between “Model-talk” and “View-talk” – which should be mute btw – and the View displays what the controller says to display.

I’ll let you check wikipedia for all the theoretical details and the thousand related frameworks.

Courtesy of the Symfony project

Meanwhile, we have tons of schema explaining to you how they work, like this one, from the Symfony framework. We’ve all seen those, but they don’t really demystify the “How do they work?“. When you look at the code, it’s MASSIVE. And it seems working like magic.

This is a humble attempt to explain the basis of how all those frameworks work for the curious programmer. I’ve been strongly inspired to create this by the Zend framework, Symfony, Yii and a great tutorial found here.

I strongly discourage you from writing your own MVC for production purposes. Use one of the above, take time to learn it and run with it. I personally have a preference for Symfony because it uses Doctrine, which is one of my favorite PHP software ever. It’s an amazing ORM.

That being said…I strongly encourage you to write your own MVC to satisfy your curiosity :)

I don’t really know where I’m going to go with this but if it proves popular, I probably will expand the whole thing, bits by bits, just for fun! If i do though, be prepared for a lot of refactoring though, since there will be many problems. I kept this as simple and straight forward as possible to keep the code distraction free.

This part is all about the dispatcher. How do we fetch the right controller, output the right data. Oh, and one more thing before we get started: we know nowadays SEO is the bread and butter of the web. Therefore, we will directly code for plain text URLs. We will go down the rabbit hole as a request hitting the web server, and see, step by step, what’s going on and how is it working.

Let’s begin!

Configuration

Software: PHP 5.2+, Apache

First thing to understand is: all the traffic goes through ONE file first. Then we dispatch it, according to the URL. To do so, we need nice URLs, and to have nice URLs we need mod_rewrite and to have a functional rewrite, we need our directory structure!!

Here is how I did it. It’s a rough mix between symfony/yii/zend, since the 3 frameworks influenced me.

Basically, the app folder contains everything that is related to your application you are working on. (in my case, a book management for example)
The lib folder contains the framework, and shouldn’t be edited between projects.
The public folder is the HTDOCS. The only thing visible to the outside world. To do so, we have to configure our webserver first:

Now configure your Apache to have your virtual domain to point to the /public directory. Here is what my virtual host file look like:

NameVirtualHost 127.0.0.1
<VirtualHost  127.0.0.1>
    ServerAdmin postmaster@dummy-host.localhost
    DocumentRoot "D:/_Dev/sites/sandbox/mvc/public"
    ServerName mvc.localhost
    ErrorLog "logs/sandbox.lh-error.log"
    CustomLog "logs/sandbox.lh-access.log" combined
</VirtualHost>

As you can see, it directly points to the public folder, which as nothing but an .htaccess file which redirect all the traffic to the index.php. let’s see the content of those file:
/public/.htaccess

RewriteEngine On
 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L]

Basically, a file or a directory that physically exists in the /public directory will be served. if not, we redirect to index.php

A visitor lands to your front page. What’s going on?

The .htaccess redirects to the index page.

/public/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
 
//simplifies our life.
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(dirname(__FILE__)));
define('LIB_PATH', ROOT . DS . 'lib' .DS . 'smvc' );
define('APP_PATH', ROOT . DS . 'app');
 
$url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
 
// validate a SEO friendly URL
 
if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED) === false)
{
   require_once(LIB_PATH . DS . 'bootstrap.php');
}
else
{
    throw new Exception('Invalid URL');
}

There is nothing special here. We define a couple of constants to make our life easier, we just make sure we got ourself a valid, SEO-friendly URL and if we do, we fire the bootstrap.
That one is the heart and soul of the MVC framework. That’s where the request is handle, so let’s go there!

/lib/smvc/bootstrap.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
/* 
 * This file is the nerveous system of the entire framework
 * It loads the config files and dispatches the requests. to the appropriate
 * controller.
 */
 
/**
 * This function automagically loads the required files, according to the name.
 * This is tighly used with the dispatcher.
 * @param string $className
 */
function __autoload($className) {
 
    if (file_exists(LIB_PATH . DS . $className . '.class.php')) {
        require(LIB_PATH  . DS . $className . '.class.php');
    } else if (file_exists(APP_PATH . DS . 'controllers' . DS . $className . '.php')) {
        require(APP_PATH . DS . 'controllers' . DS . $className . '.php');
    } else if (file_exists(APP_PATH . DS . 'models' . DS . $className . '.php')) {
        require(APP_PATH . DS . 'models' . DS . $className . '.php');
    } else {
        throw new Exception('Class not found');
    }
}
 
function dispatcher() {
 
    //'car' stands for Controller Action and Request
    $car = Smvc_Router::route($_SERVER['REQUEST_URI']);
 
    $cname = ucfirst($car[0][0]);
    $controller = $cname . 'Controller';
    $action = ucfirst($car[0][1]);
    $request = explode('/',$car[1]);
 
    $triggeredController = new $controller($request);
    call_user_func(array($triggeredController,'execute'.$action) , $request);
 
    extract($triggeredController->getViewVariables());
    ob_start();
    include(APP_PATH . DS . 'views' . DS . $cname . DS . $action . '.php');
    $smvc_content = ob_get_contents();
    ob_end_clean();
    include(APP_PATH . DS . 'views' . DS . 'layout.php');
    echo "\n\n";
 
}
 
dispatcher();

The function __autoload($className) is fired every time we call a class an PHP can’t find it. Obviously since we don’t include() any other file than the bootstrap, we won’t have any class loaded. So as you can see, to keep this system as light as possible, we load classes on demand only. This is crucial to keep performance independent of the size of the framework. You can have a hundred controllers and views, it shouldn’t be any much slower than if you have only a view and a controller. This function will probably be refactored in the future as it can be improved, but for now, I like its simplicity.

Now the dispatcher. As you can see, it’s the only function called in the scripts (for now). Everything else is class based. Once inside it, the first thing we do is analyze the URL with the router. I chose to put it in a class, it’s not really necessary at this point the function could have been included in the bootstrap.

/lib/smvc/Smvc_Router.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
/* 
 * This class finds the right Controller and Action according to the passed URL
 * and Routes.
 */
 
/**
 * Description of Router
 *
 * @author PLeblanc
 */
final class Smvc_Router {
 
    public static function route($url) {
 
        require_once(APP_PATH . DS . 'config' . DS . 'routes.php');
 
        foreach ( $routes as $pattern => $controllerAndAction ) {
            if ( preg_match('/^'. $pattern . '/', $url ) ) {
                $requestString = preg_replace('/^'.$pattern . '/', '', $url,1);
 
                return array($controllerAndAction, $requestString);
            }
	}
 
        //we should always find a route. If not, there is problem.
        throw new Exception('No routes were found');
 
    }
 
 
}

This file immediately fetches the routes for the application. For now we’re keeping things simple: the route is a simple Regex expression checked against the URL. If it’s found, we return the associated Controller and Action, with the remainder of the URL. Indeed, we remove the matching pieces from the URL since we assume it’s a description of a controller and an action, and consider the rest as arguments to be processed by the Controller.

Here is my route file:
/app/config/routes.php

<?php
/* 
 * This is the routing files. All URLs combinaitions should be written here.
 * The order is important as it will search from top to bottom.
 */
 
$routes = array (
        '\/book\/' => array('Book', 'Index')
    );

So this means that we are looking for a aurl such as mydomain.com/book/
This will send to the function dispatcher() the Controller, the Action, and the rest of the url, that will be treated as argument(s).

From line 31 to 34 in /lib/smvc/bootstrap.php, we prepare the variables to fit a convention. This helps keeping the code tidy. This is what I arbitrarily decided:

  • Classes files always start with a capital letter
  • Classes files are named after the class they contain
  • Controllers must have “Controller” appended to it, such as BookController.php
  • URL Actions inside controllers must start by “execute“, such as executeIndex
  • View folders and actions are have a leading capital letter too
  • I decided that ‘/’ represent break in arguments/routes. It makes sense in most SEO situations.

Then line 36, we simply create a new controller object, with the request as argument. In this case, if the path is /book/ it will match the first route and return the BookController, which is going to be instantiated.

36
$triggeredController = new $controller($request);

What we’re effectively doing in the case of the user trying to access mydomain.com/book/hello-world/how-are-you:

$triggeredController = new $BookController(array('hello-world','how-are-you')); //remember we explode the request on slashes ( '/' ) on line 34

This is what the BookController looks like:
/app/controllers/BookController.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
/**
 * Description of BookController
 *
 * @author PLeblanc
 */
class BookController extends Smvc_Controller {
 
    public function executeIndex() {
        $this->myRequest = 'This is my request: ' . $this->_request[0];
    }
}

As you can see, extremely basic for now. We’re just storing the 1st argument in a variable that is going to be used in the view. All the variables set like “$this->myVariable” are accessible to the view. We’re getting there in a minute. Meanwhile, the astute reader noticed we are extending an parent class. Here it is:

/lib/smvc/Smvc_Controller.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
/**
 * Description of Smvc_Controllerclass
 *
 * @author PLeblanc
 */
abstract class Smvc_Controller {
 
    protected $_request;
    protected $_viewVariables;
 
 
    public function __construct($request) {
        $this->_request = $request;
    }
 
    public function __set($name, $args) {
        $this->_viewVariables[$name] = $args;
    }
 
    public function __get($name) {
        return $this->_viewVariables[$name];
    }
 
    public function getViewVariables() {
        return $this->_viewVariables;
    }
 
}

As you can see, it’s a very simple and basic container class. We are using magic getter and setter to store all variables in a array. The request, meanwhile, is being stored by the constructor, so it’s readily available inside our controller as “$this->_request”.

In line 37, once the object is instantiated, we call the action associated by the matching route.

call_user_func(array($triggeredController,'execute'.$action))

The above code calls /app/controllers/BookController.php

    public function executeIndex() {
        $this->myRequest = 'This is my request: ' . $this->_request[0];
    }

As I said earlier on, all the variables set in the controller are then extracted in line 39. We’re doing this because the variables are then easily used in the view. Now we start the output buffer. This is because we want to have the view rendered in to a variable. We can then inject the view inside the layout file. This is a good practice because we don’t have broken HTML files (such as a traditional Header.php + Footer.php, like Wordpress is doing).

Here is the view used to respond to the index action:

/app/views/Book/Index.php

<h1>You made a request for the BOOK CONTROLLER</h1>
 
<p>And this is the first argument passed:</p>
 
<div style="background-color: #DDF; padding: 25px;">
    <?php echo $myRequest ?>
</div>

As you can see, we can use EVERY variables used in the controller directly in the view. If, in the controller, I used $this->helloWorld, I will be able to use $helloWorld in my view, thanks to the extract() line 39 of the bootstrap.php.
and finally the extremely basic layout page. as you can see, the layout path is hardcoded in the bootstrap. It’s probably something we will want to change in the future, but for now, it’s doing the job.

/app/views/layout.php

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
  </head>
  <body>
    <?php echo $smvc_content ?>
  </body>
</html>

So there, you have it!

If you try to access it now, with this URL: http://mvc.localhost/book/hello-world you’re going to have this result:
————————————

You made a request for the BOOK CONTROLLER

And this is the first argument passed:

This is my request: hello-world

————————————-
However if you’re trying any other urls that don’t start with “book”, it’s gonna throw a fatal error, since no routes have been created for them! for example: http://mvc.localhost/hello-world

Fatal error: Uncaught exception 'Exception' with message 'No routes were found' in D:\_Dev\sites\sandbox\mvc\lib\smvc\Smvc_Router.class.php:27 Stack trace: #0
D:\_Dev\sites\sandbox\mvc\lib\smvc\bootstrap.php(29): Smvc_Router::route('/hello-world') #1 D:\_Dev\sites\sandbox\mvc\lib\smvc\bootstrap.php(49): dispatcher() #2
D:\_Dev\sites\sandbox\mvc\public\index.php(15): require_once('D:\_Dev\sites\s...') #3 {main} thrown in D:\_Dev\sites\sandbox\mvc\lib\smvc\Smvc_Router.class.php on line 27

Recap

It’s a lot to absorb. It’s going to take a while, but I really tried to keep things as simple as possible to begin this tutorial. Take some time to reproduce it step by step. I’ve included all the code in quotes, nothing is hidden. If you do it step by step and recode this MVC from scratch, you’ll have a much better understanding of how all this works.

Have fun!

Read more from PHP
2 Comments Post a comment
  1. maiconcarlos
    Apr 7 2010

    Superb! Crazy! Wonderful! I loved it! Thanks

  2. maiconcarlos
    Apr 7 2010

    maiconcarlos :
    Superb! Crazy! Wonderful! I loved it! Thanks

    I’m waiting for part 2. It was awesome!

Share your thoughts, post a comment.

(required)
(required)

Note: HTML is allowed. Your email address will never be published.

Subscribe to comments