Reinventing a PHP MVC framework, part 2 of 4
This is part 2 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
Let's make the wheel more round
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 ASP.NET MVC, responding to an HTTP request is done by returning an instance of a class derived from the abstract ActionResult
class. For a normal page view, you return a ViewResult
object. For an AJAX request expecting JSON data, you return a JsonResult
object. Some other examples are the RedirectResult
, HttpStatusCodeResult
, AtomFeedActionResult
, and FileContentResult
classes.
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 Router
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 the perform a standard PHP include
on a view php file, using a (mocked) filesystem wrapper.
// Testing the ViewResult
class ViewResultTests {
public function ExecuteResultSetsCorrectContentType() {
$controllerName = 'Home';
$viewName = 'Index';
$model = null;
$viewResult = new ViewResult($controllerName, $viewName, $model);
Expect($response)->toCall('setHeader')->withArguments(['Content-Type', 'text/html; charset=utf-8']);
Expect($viewRootDir)->toCall('phpInclude')->withArguments(['home/index.php']);
$viewResult->executeResult($response, $viewRootDir);
$response->checkAll();
$viewRootDir->checkAll();
}
}
// First implementation of ViewResult
class ViewResult {
private $controllerName;
private $viewName;
public function __construct($controllerName, $viewName, $model) {
$this->controllerName = $controllerName;
$this->viewName = $viewName;
}
public function executeResult($response, $viewRootDir) {
$response->setHeader('Content-Type', 'text/html; charset=utf-8');
$viewRootDir->phpInclude(mb_strtolower($this->controllerName) . '/' . mb_strtolower($this->viewName) . '.php');
}
}
The constructor for the ViewResult
class needs the name of the controller, not the controller class. For this, we need to add a bit more code 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.
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: