Reinventing a PHP MVC framework, part 2 of 4

#php #mvc #learning #reverse-engineering

Written by Anders Marzi Tornblad

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: