Reinventing a PHP MVC framework, part 4

Putting the parts together

This is the fourth part of a series of articles about the mt-mvc PHP MVC framework. If you haven't read the first parts, here is the first one: Reinventing a PHP MVC framework, part 1

The ASP.NET MVC framework started out pretty simple, but now contains loads of features that I don't really feel belong in something called an MVC framework. There is stuff like script and stylesheet bundling and minification, helper methods for HTML "controls", the Action Filters from MVC 3, and so on.

These things are nice to have, but they bloat the framework and should be added using some smart Dependency Injection framework instead.

That is why, in my PHP MVC implementation, I will stick (for now) with the most basic things needed to get some real use out of an MVC framework:

  • Routing URLs matching a pre-set pattern to Controller class names and Views
  • Getting user input from the URL or POST data without hassle
  • Rendering an HTML view
  • Robust, unit-tested code

Url rewriting

To be able to use urls like this one: in PHP, you need to do some url rewriting. This section in .htaccess should work fine for you:

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteRule ^(.*) mvc.php?path=$1 [L]

Or this setting in IIS UrlRewrite web.config:

<!-- web.config -->
<?xml version="1.0" encoding="UTF-8"?>
                <rule name="MVC routing" stopProcessing="true">
                    <match url="^(.*)" ignoreCase="false" />
                    <action type="Rewrite" url="mvc.php?path={R:1}" />

Then getting the requested path is as simple as calling:

$requestedPath = $_GET['path'];

Autoloading magic

Taking a convention over configuration approach, I simply decide that controller classes should belong to the Controllers namespace, reside in the ~/Controllers/ directory, and have a name ending with Controller. This is in line with how ASP.NET MVC expects things, in the default setting.

By registering an autoload function, I get a certain amount of control over what .php files to include when some part of the solution wants to create an instance of a controller class. The spl_autoload_register function takes a callback function that gets called every time an unknown class is referenced. The callback function can either create the class (probably by including some php file containing the class) and return true, or decide that it is not the right callback for the job, and return false.

The autoloader for MVC controller classes looks like this:

// mvc-controller-autoloader.php

spl_autoload_register(function ($fullClassName) {
    // Must be Namespace\Classname
    $parts = explode('\\', $fullClassName);
    $isTwoParts = (count($parts) == 2);
    if (!isTwoParts) return false;

    // Namespace must be 'Controllers'
    $namespaceName = $parts[0];
    $isControllersNamespace = ($namespaceName == 'Controllers');
    if (!isControllersNamespace) return false;

    // Class name must end with 'Controller'
    $className = $parts[1];
    $isControllerSuffix = (substr($className, -10) == 'Controller');
    if (!isControllerSuffix) return false;

    // Look for file here: DOCUMENT_ROOT/Controllers/classname.php
    $filename = $_SERVER['DOCUMENT_ROOT'] .
                DIRECTORY_SEPARATOR .
                'Controllers' .
                DIRECTORY_SEPARATOR .
                $className . '.php';

    // Does the file exist?
    if (!file_exists($filename)) return false;

    // Include the file. Done!
    require_once $filename;
    return true;

Now that the routing mechanism is (almost) in place, and there is a way of locating the controller classes, putting things together will look something like this:

// mvc.php

$routing = new Routing();
$route = $routing->handle($requestedPath);
$controllerClassName = 'Controllers\\' . $route->controllerClassName;

if (class_exists($controllerClassName)) {
    $controllerClass = new ReflectionClass($controllerClassName);

    if ($controllerClass->hasMethod($route->methodName)) {
        $controllerInstance = new $controllerClassName;

        $method = $controllerClass->getMethod($route->methodName);

        $inputModelBuilder = new InputModelBuilder;

        // TODO: Create $request instance first
        $inputModel = $inputModelBuilder->buildInputModel($method, $request, $route);

        $result = $method->invokeArgs($controllerInstance, $inputModel);

        // TODO: Create $response and $viewRootDir instances first
        $result->executeResult($response, $viewRootDir);
    } else {
        // Non-existing method!
        // Respond with 404 Not Found
} else {
    // Non-existing controller!
    // Respond with 404 Not Found

Still to do

There is still some code to write before this framework is useful. As you have noticed, some code was mocked in the unit tests of the earlier articles of this series:

  • Some Request class that contains POST data (and possibly other things in the future)
  • Some Response class that knows how to set HTTP headers (and possibly other things in the future)
  • Some FileSystem class that wraps files, directories and that can include files into the output stream
  • Some AutoLoader that loads the different parts of the framework when needed
  • Some ActionResult base class that ViewResult and other result classes can inherit from
  • Some NotFoundResult class that sets the HTTP response to 404
  • Consider putting the framework classes in a namespace of their own

These action points will be the topic of the next article(s). For now, take care!

You'll find the code from this article in the related release on GitHub. The latest version is always available in the GitHub repository.