Reinventing a PHP MVC framework, part 1 of 4

#php #mvc #learning #reverse-engineering

Written by Anders Marzi Tornblad

This is part 1 of the Reinventing a PHP MVC framework series.

Let's reinvent the wheel

I wanted to know how ASP.NET MVC does what it does, so I decided to find out ... by trying to reinvent it... in PHP. My line of thought was this:

I am well 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, just 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 colely 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.

// First batch of tests

class RoutingTests {
    public function CheckAllDefaults() {
        $routing = new Routing();
        $route = $routing->handle('');
        The($route->controllerClassName)->shouldEqual('HomeController');
        The($route->methodName)->shouldEqual('Index');
        The($route->parameter)->shouldNotBeSet();
    }
    
    public function CheckDefaultMethodNameAndParameter() {
        $routing = new Routing();
        $route = $routing->handle('Articles');
        The($route->controllerClassName)->shouldEqual('ArticlesController');
        The($route->methodName)->shouldEqual('Index');
        The($route->parameter)->shouldNotBeSet();
    }
    
    public function CheckDefaultParameter() {
        $routing = new Routing();
        $route = $routing->handle('Categories/List');
        The($route->controllerClassName)->shouldEqual('CategoriesController');
        The($route->methodName)->shouldEqual('List');
        The($route->parameter)->shouldNotBeSet();
    }
    
    public function CheckNoDefaults() {
        $routing = new Routing();
        $route = $routing->handle('Products/Item/123x');
        The($route->controllerClassName)->shouldEqual('ProductsController');
        The($route->methodName)->shouldEqual('Item');
        The($route->parameter)->shouldEqual('123x');
    }
}

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.

// First chunk of code

class Routing {
    public function handle($url) {
        $parts = explode('/', $url);
        $controllerName = @$parts[0];
        $methodName = @$parts[1];
        $parameter = @$parts[2];
        
        if (!$controllerName) $controllerName = 'Home';
        if (!$methodName) $methodName = 'Index';
        
        return (object) [
            'controllerClassName' => $controllerName . 'Controller',
            'methodName' => $methodName,
            'parameter' => $parameter
        ];
    }
}

Usefulness right now

This class does the base 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 connect views to the controller methods, and so on. Don't worry; all of that will be covered in the following posts.

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: