Reinventing a PHP MVC framework, part 4 of 4
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:
- 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: 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:
- 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 thatViewResult
and other result classes can inherit from - Some
NotFoundResult
class that sets the HTTP response to404
- Consider putting the framework classes in a namespace of their own
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: