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
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/ninjas/item/52 in PHP, you need to do some url
rewriting. This section in .htaccess should work fine for you:
#.htaccess
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule ^(.*) mvc.php?path=$1 [L]
</IfModule>
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.phpspl_autoload_register(function ($fullClassName) {
// Must be Namespace\Classname$parts = explode('\\', $fullClassName);
$isTwoParts = (count($parts) == 2);
if (!isTwoParts) returnfalse;
// Namespace must be 'Controllers'$namespaceName = $parts[0];
$isControllersNamespace = ($namespaceName == 'Controllers');
if (!isControllersNamespace) returnfalse;
// Class name must end with 'Controller'$className = $parts[1];
$isControllerSuffix = (substr($className, -10) == 'Controller');
if (!isControllerSuffix) returnfalse;
// 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)) returnfalse;
// Include the file. Done!require_once$filename;
returntrue;
});
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 = newRouting();
$route = $routing->handle($requestedPath);
$controllerClassName = 'Controllers\\' . $route->controllerClassName;
if (class_exists($controllerClassName)) {
$controllerClass = newReflectionClass($controllerClassName);
if ($controllerClass->hasMethod($route->methodName)) {
$controllerInstance = new$controllerClassName;
$method = $controllerClass->getMethod($route->methodName);
$inputModelBuilder = newInputModelBuilder;
// 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!
This is the third 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
When an HTTP request is handled by ASP.NET, a factory called DefaultControllerFactory goes to
work. Its CreateController method looks through the web app's controller classes using reflection,
finds the right one and creates an instance. Then the infrastructure takes a look at which method to
call, again using reflection, and the method gets called. The result, some ActionResult subclass,
then produces the correct output.
The mechanism relies heavily on reflection which is really difficult to unit-test, because you would
have to mock the entire class system and file system. So this part is not developed using TDD, but
write-and-debug. I'm sorry!
PHP doesn't have precompiled assemblies that the infrastructure can search through to find the correct
class. Instead we need to take a convention over configuration approach,
and pick some standards to enforce. Using PHP's autoload capabilities, we can
write an autoloader specifically for controllers. The "controller factory" then simply turns into
a call to class_exists. The autoloader is detailed in a future article.
Target in sight
The next piece of the puzzle is what I would like to call the MVC framework itself – a class that
binds request, routing and response together. The request and response are mocked, and the workflow
looks a little like this:
Get path of url from request handler
Translate path into route information using the routing system
Check the method signature of the method requested:
If the method requires a single piece of trivial input, and the parameter is called $id, first try the parameter value from the route information
If the method requires additional or non-trivial input, get post data from request handler, using prefixed or non-prefixed keys
Build the required input model, if any
Because the input model will get passed to a method, the input model is an array of arguments in correct order for passing to the method
Run the method, passing the input model as arguments
Execute the result using the response handler
The routing bit is already in place, albeit not complete for non-trivial production purposes. The next
step is creating an input model builder, and here are the first set of tests for it:
Making these tests succeed is not that hard, but I have to admit that we have strayed a little from the ASP.NET path now. This is intentional. I want the ease-of-use of ASP.NET, but also want to add some ideas of my own.
// InputModelBuilder implementationclassInputModelBuilder {
publicfunctionbuildInputModel(ReflectionMethod$method, $request, $route) {
$parameters = $method->getParameters();
$parameterCount = count($parameters);
if ($parameterCount === 0) {
return [];
}
$result = [];
foreach ($parametersas$index => $parameter) {
$typeHint = $parameter->getClass();
$name = $parameter->getName();
// Trivial single-value input model named $id:if ($name == 'id' && !isset($typeHint) && $parameterCount === 1) {
// This is the only time the $route->parameter is used!$value = @$route->parameter;
if (isset($value)) {
return[$value];
}
}
if (!isset($postData)) $postData = $request->post;
if (!isset($typeHint)) {
// Trivial single value from postif (isset($postData[$name])) {
$result[] = $postData[$name];
}
} else {
// Type-hinted value$result[] = $this->buildTypeHintedObject($name, $typeHint, $postData);
}
}
return$result;
}
privatefunctionbuildTypeHintedObject($optionalPrefix, ReflectionClass$typeHint, array$postData) {
$className = $typeHint->getName();
$result = new$className;
$properties = $typeHint->getProperties();
foreach ($propertiesas$property) {
$name = $property->getName();
$prefixedName = "$optionalPrefix-$name";
if (isset($postData[$prefixedName])) {
$property->setValue($result, $postData[$prefixedName]);
} elseif (isset($postData[$name])) {
$property->setValue($result, $postData[$name]);
}
}
return$result;
}
}
The bits and pieces are starting to fall into place, but they all need to be put
together. Come back here in a while for more on that.
In the old days, before fire was invented, responding to a request was done by
calling Response.Write and writing directly
to the response stream. In an MVC world (in whatever language, but especially in an object-oriented one), doing
this from within a controller is a big no-no. Writing to the stream,
using Response.Write or echo will only happen in the View!
Most of those classes reference some model object, and will eventually render something view-like using
the properties of the model object. The rendering itself, including sending HTTP headers, takes place in an
implementation of the abstract ExecuteResult method.
For now, I will focus only on serving ordinary views, like ASP.NET MVC does through the ViewResult class.
Some assembly needed
Using the Routing class from the previous part, we can find the names
of a controller class and the method to call. We will now expect that method to return an object that has
an executeResult method (first letter is lower-case, because PHP). I actually want to make my MVC framework
act more in line with the MVC pattern than ASP.NET.
First of all, I don't want the controller to be able to access response artefacts like HTTP response headers,
and the response content stream, because those are definitely presentation details. To have
a clear separation of duties, those
things should only be available to the View. Because of this, the executeResult method needs to be provided with
some mechanism for setting HTTP headers and writing content. This "response wrapper" is easily mocked, for now. For
testability, we also need to mock the filesystem.
This first iteration of ViewResult should set the Content-Type to text/html and then perform a standard
PHP include on a view php file, using a (mocked) filesystem wrapper.
The constructor for the ViewResult class needs the name of the controller, not the controller class. For
this, we need to add a few more lines to the RoutingTests and Routing classes. That code is trivial and out
of scope for this article, but you can look at it in the GitHub release.
I know the effects of using the various features of the ASP.NET MVC framework
I know the principles of TDD
I should be able to reinvent (or reverse-engineer) a working MVC framework by adding unit tests for increasingly complex use of MVC, and making one or a few tests pass at a time
I also want to become a better PHP developer
I am perfectly aware of the fact that there are lots of MVC frameworks for PHP that are really
capable of taking care of business, but this is not a website development effort. This is a
learning effort. Reinventing the wheel works fine for learning - not for production code.
MVC the ASP.NET way
Let's start with something simple. The most basic use of ASP.NET MVC, in the default setting, appears
to work by separating the request path of an incoming request into a Controller class name,
a View method name, and an optional parameter value that gets passed into the method. Also,
there are default values for all parts of the path.
First set of tests
I imagine a class that's solely responsible for parsing a path, and suggesting the name of a controller
class, and a method to call, so I write some tests for that class first. Hooking things up to the PHP
HTTP infrastructure gets added later.
These tests are about the default out-of-the-box behavior of the routing subsystem. More advanced features, like registering custom url patterns, get added later.
Usefulness right now
This class does the bare minimum, and making some real use of it requires a lot of nuts and bolts in place – some URL redirection, a request/response pipeline system, some use of reflection to dynamically create controller instances and calling methods, a lot of thought about how to connecting views to the controller methods, and so on. Don't worry; all of that will be covered in the following posts.