HiveBrain v1.2.0
Get Started
← Back to all entries
patternphpMinor

PHP Router for MVC

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
mvcphprouterfor

Problem

I recently started developing my first serious project in PHP, a MVC completely build by myself, which is quite a challenge, when being new to OOP.

So the main struggle of the whole project has, and is, my router. It's really important, so I want to have a... great?... router, that just works - but without compromising functionality.

Creating a router in itself wasn't really a challenge (Might be because I do it a wrong way), but after having made a basic router that did the job, I realized the 2 main problems. GET functionality and folders.

The challenge is pretty URLs AND being able to load controllers from specific folder (or subfolders of it), executing specific method and passing in GET values.

I tried, and it works. But it's really just a mess:

```
class router {

/**
* Contains the path of the controller to be loaded.
* This will be extended if the "bit" does not exist as a file.
* This makes it possible to put controllers into folder.
*
*/
public static $path = null;

/**
* Contains the name of the file to be loaded.
* This is also used when instantiating the controller, as the name of the class inside the file, and the name of the file, should be identical.
*
*/
public static $file = null;

/**
* Contains the name of the method to be run inside the controller
*
*/
public static $method = null;

/**
* This function is run at the index file, and should only be run once. Running it twice serves no purpose, and will just result in errors.
* It fetches the controller, and runs specified method inside the controller - or shows error 'notfound'.
* If more parameters are specified than just the path of the controller, the controllers name and the method to be run, it will pass theese to the
* get helper, if loaded.
*
* The get helper must be autoloaded though, because the get parameters are passed to the get helper before running the conten

Solution

Unfortunately, this is a very large question to ask. So there will be gaps in my answer but i will answer it best i can using code where possible.

You do not want to couple your router with the controllers. They both do different things. If you do combine them, you end up with a mess (as you have found out).

class Router {

    private $routes = array();

    public function addRoute($pattern, $tokens = array()) {
        $this->routes[] = array(
            "pattern" => $pattern,
            "tokens" => $tokens
        );
    }

    public function parse($url) {

        $tokens = array();

        foreach ($this->routes as $route) {
            preg_match("@^" . $route['pattern'] . "$@", $url, $matches);
            if ($matches) {

                foreach ($matches as $key=>$match) {

                    // Not interested in the complete match, just the tokens
                    if ($key == 0) {
                        continue;
                    }

                    // Save the token
                    $tokens[$route['tokens'][$key-1]] = $match;

                }

                return $tokens;

            }
        }

        return $tokens;

    }
}


That class (which i quickly wrote just now so it's not perfect and may need adjusting to suit your use case).

Then to use the class you would register your "routes" and then parse something through them (i.e. the URL)

$router = new Router();
$router->addRoute("/(profile)/([0-9]{1,6})", array("profile", "id"));
$router->addRoute("/(.*)", array("catchall"));
print_r($router->parse($_SERVER['REQUEST_URI']));


If you browse to /profile/23232 It will return:

Array ( [profile] => profile [id] => 23232 )

If you browse to /asdasd

Array ( [catchall] => asdasd )

That's your router done.

Next step. Execute the controller.

First step, is to create a factory (i've spoken about this in more detail on my blog "Practical use of the Factory Pattern, Polymorphism and Interfaces in PHP"). The factory will then take the router and combine it with your logic to construct the correct controller instance which it then returns.

This code (using the Router class above):

$router = new Router();
$router->addRoute("/(profile)/([0-9]{1,6})", array("profile", "id"));
$router->addRoute("/Controller/(Login)", array("Controller"));
$router->addRoute("/(.*)", array("catchall"));

abstract class Controller {
    abstract public function execute();
}

class ControllerLogin extends Controller {
    public function execute() {
        print "Logged in...";
    }
}

class ControllerFactory {
    public function createFromRouter(Router $router) {
        $result = $router->parse($_SERVER['REQUEST_URI']);
        if (isset($result['Controller'])) {
            if (class_exists("Controller{$result['Controller']}")) {
                $controller = "Controller{$result['Controller']}";
                return new $controller();
            }
        }
    }
}

$factory = new ControllerFactory();
$controller = $factory->createFromRouter($router);
if ($controller) {
    $controller->execute();
} else {
    print "Controller Not Found";
}


Will output Logged in... if you enter /Controller/Login in the URL. Controller Not Found if you enter any other URL.

If you fully understand the above and mix it in with your own custom style and requirements. You will be able to create a very flexible code design :) Who says routes must come from the URL (they could come from the command line). No problem if you decouple the router from the controllers (which was achieved by using the factory as the middle man).

There is still much you can add to refine it such as the way the controllers are loaded is very "raw" to say the least. You would also want to tweak the error reporting if the router/controller can't be found and create the routes in the format you desire.

Have fun :)

Code Snippets

class Router {

    private $routes = array();

    public function addRoute($pattern, $tokens = array()) {
        $this->routes[] = array(
            "pattern" => $pattern,
            "tokens" => $tokens
        );
    }

    public function parse($url) {

        $tokens = array();

        foreach ($this->routes as $route) {
            preg_match("@^" . $route['pattern'] . "$@", $url, $matches);
            if ($matches) {


                foreach ($matches as $key=>$match) {

                    // Not interested in the complete match, just the tokens
                    if ($key == 0) {
                        continue;
                    }

                    // Save the token
                    $tokens[$route['tokens'][$key-1]] = $match;

                }

                return $tokens;

            }
        }

        return $tokens;

    }
}
$router = new Router();
$router->addRoute("/(profile)/([0-9]{1,6})", array("profile", "id"));
$router->addRoute("/(.*)", array("catchall"));
print_r($router->parse($_SERVER['REQUEST_URI']));
$router = new Router();
$router->addRoute("/(profile)/([0-9]{1,6})", array("profile", "id"));
$router->addRoute("/Controller/(Login)", array("Controller"));
$router->addRoute("/(.*)", array("catchall"));

abstract class Controller {
    abstract public function execute();
}

class ControllerLogin extends Controller {
    public function execute() {
        print "Logged in...";
    }
}

class ControllerFactory {
    public function createFromRouter(Router $router) {
        $result = $router->parse($_SERVER['REQUEST_URI']);
        if (isset($result['Controller'])) {
            if (class_exists("Controller{$result['Controller']}")) {
                $controller = "Controller{$result['Controller']}";
                return new $controller();
            }
        }
    }
}

$factory = new ControllerFactory();
$controller = $factory->createFromRouter($router);
if ($controller) {
    $controller->execute();
} else {
    print "Controller Not Found";
}

Context

StackExchange Code Review Q#54399, answer score: 9

Revisions (0)

No revisions yet.