Reinventing a PHP MVC framework, part 4 of 4

#php #mvc #learning #reverse-engineering

Written by Anders Marzi Tornblad

This is part 4 of the Reinventing a PHP MVC framework series. If you haven't read the first part, here it is: Reinventing a PHP MVC framework, part 1 of 4

Putting the parts together

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 in 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:

Url rewriting

To be able to use urls like this one: http://example.com/cats/showKitty/52 in PHP, you need to do some url rewriting. This section in .htaccess should work fine for Apache:

# .htaccess

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

Or this setting in IIS Rewrite web.config:

<!-- web.config -->

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

Then getting the requested path is as simple as calling:

$requestedPath = $_GET['path'];

Autoloading magic

Taking a convention over configuration approach, I decide that controller classes must 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:

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

Articles in this series: