Anders Tornblad

All about the code

Monthly archive for May 2015

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: http://example.com/ninjas/item/52 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] </IfModule>

Or this setting in IIS UrlRewrite 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 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:

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:

$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!

Reinventing a PHP MVC framework, part 1
Reinventing a PHP MVC framework, part 2
Reinventing a PHP MVC framework, part 3
Reinventing a PHP MVC framework, part 4 (this part)

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

Reinventing a PHP MVC framework, part 3

Let's add some spokes to the wheel

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 view.

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:

  1. Get path of url from request handler
  2. Translate path into route information using the routing system
  3. 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
  4. Run the method, passing the input model as arguments
  5. 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:

class InputModelBuilderTests { public function ReturnsNullForMethodWithoutParameters() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('NoParametersMethod'); // No expected calls to request or route! Expect($route); Expect($request); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); The($result)->ShouldEqual([]); } public function ReturnsRouteParameterForSimpleParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SimpleParameterMethod'); // No expected calls to request! Expect($route)->toGet('parameter', 'abc.123'); Expect($request); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The($result)->ShouldEqual(['abc.123']); } public function ReturnsPostIdForSimpleParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SimpleParameterMethod'); Expect($route)->toGet('parameter', null); Expect($request)->toGet('post', ['id' => 'abc.123']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The($result)->ShouldEqual(['abc.123']); } public function ReturnsPostDataForTwoSimpleParametersMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('TwoSimpleParametersMethod'); // No expected calls to route! Expect($route); Expect($request)->toGet('post', ['bar' => 'QWER', 'foo' => 'ASDF']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The($result)->ShouldEqual(['ASDF', 'QWER']); } public function ReturnsNonprefixedPostDataForComplexParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SmallInputModelMethod'); // No expected calls to route! Expect($route); Expect($request)->toGet('post', ['bar' => 'QWER', 'foo' => 'ASDF']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The(count($result))->ShouldBeExactly(1); The($result[0])->ShouldBeInstanceOf(\ImbtSmallInputModel::class); The($result[0]->foo)->ShouldEqual('ASDF'); The($result[0]->bar)->ShouldEqual('QWER'); } public function ReturnsPrefixedPostDataForComplexParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SmallInputModelMethod'); // No expected calls to route! Expect($route); Expect($request)->toGet('post', ['model-bar' => 'QWER', 'model-foo' => 'ASDF']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The(count($result))->ShouldBeExactly(1); The($result[0])->ShouldBeInstanceOf(\ImbtSmallInputModel::class); The($result[0]->foo)->ShouldEqual('ASDF'); The($result[0]->bar)->ShouldEqual('QWER'); } } class ImbtControllerDummy { public function NoParametersMethod() { } public function SimpleParameterMethod($id) { } public function TwoSimpleParametersMethod($foo, $bar) { } public function SmallInputModelMethod(ImbtSmallInputModel $model) { } } class ImbtSmallInputModel { public $foo; public $bar; }

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.

class InputModelBuilder { public function buildInputModel(ReflectionMethod $method, $request, $route) { $parameters = $method->getParameters(); $parameterCount = count($parameters); if ($parameterCount === 0) { return []; } $result = []; foreach ($parameters as $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 post if (isset($postData[$name])) { $result[] = $postData[$name]; } } else { // Type-hinted value $result[] = $this->buildTypeHintedObject($name, $typeHint, $postData); } } return $result; } private function buildTypeHintedObject($optionalPrefix, ReflectionClass $typeHint, array $postData) { $className = $typeHint->getName(); $result = new $className; $properties = $typeHint->getProperties(); foreach ($properties as $property) { $name = $property->getName(); $prefixedName = "$optionalPrefix-$name"; if (isset($postData[$prefixedName])) { $property->setValue($result, $postData[$prefixedName]); } else if (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.

Reinventing a PHP MVC framework, part 1
Reinventing a PHP MVC framework, part 2
Reinventing a PHP MVC framework, part 3 (this part)
Reinventing a PHP MVC framework, part 4

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