6
\$\begingroup\$

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 content of the controller.
     */
    public static function dispatch() {
        // Remove Simple MVC's location from request
        $request = str_replace(config::get('routes/path'), '', $_SERVER['REQUEST_URI']);
        if(!$request) {
            $request = config::get('routes/defaultController');
        }
        // Split request into array
        $path = explode('/', $request);
        // Go trough array
        foreach($path as $bit) {
            // Check whether the method already has been set
            if(self::$method == null) {
                // Check whether to file to load already has been set
                if(self::$file == null) {
                    // Check if a file exists at the already defined 'path' with the name of bit
                    if(file_exists('app/controllers/' . self::$path . $bit . '.php')) {
                        // Set file name
                        self::$file = $bit;
                        // Require in the file
                        require('app/controllers/' . self::$path . self::$file . '.php');
                        // Instantiate controller
                        $controller = new self::$file();
                    // If file does not exist
                    } else {
                        // Add the bit to the path
                        self::$path .= $bit . '/';
                    }
                // If file already has been loaded
                } else {
                    // Check whether 'bit' method exists in the controller
                    if(method_exists($controller, $bit)) {
                        // Set method to 'bit'
                        self::$method = $bit;
                    }
                }
            // If method already has been defined, check whether the 'get' helper has been loaded
            } elseif(helper::isLoaded('get')) {
                // Push 'bit' to the 'get' helper
                array_push(get::$values, $bit);
            }
        }

        // Check whether the controller has been instantiated
        if(isset($controller)) {
            // Check whether the method has been set
            if(self::$method == null) {
                // If method has not been set, run defaultFunction
                $controller->defaultFunction();
            // If method has been set
            } else {
                // Run defined method
                $method = self::$method;
                $controller->$method();
            }
        // If controller is not set
        } else {
            // Show 'notfound' error
            error::show('notfound');
        }

    }

}

The dispatch() method is simply run at the index file, which the .htaccess file just redirects all requests to.

The request is then split into an array, which a foreach then go through and first of it tries to find a file with the name of 'bit', if it does not exist it add the 'bit' to the path from the controllers folder. This makes subfolders possible. When it has the file, it continues until it has a method. When a method is defined, all remaining 'bits' get passed to an array in the 'GET' helper.

But this being such a mess, I'm hoping for opinions and advice.

\$\endgroup\$
7
\$\begingroup\$

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 :)

\$\endgroup\$
2
\$\begingroup\$
  • Statics! Statics everywhere! - Static methods and variables are global, and are not a part of OOP. Do not use static methods and properties. There are plenty disadvantages with statics and no real benefits.
  • Naming convention - ClassNames should be capitalized. That makes it easier to distinguish new Route() from $var = router().
  • How are you dispatching? - Based on what? URI? Text files? Where do you get your routes from?

Better approach

It isn't the Router's job to dispatch your request, or initialize the controller. So we will make a total of 2 classes:

  1. Router - Which is the component which takes a URI (or if you're really fancy, a Request object), and spits out an object that represents the matched route.
  2. Route - is an object that represents a single route. It contains information about the URI pattern, the controller and action, and the variables that need to be passed into them.

Interface for Router:

interface Router {
    public function addRoute(Route $route);
    public function route($uri); //Returns a Route object.
}

Interface for Route:

interface Route {
    //Pattern can be anything you want. 
    //If you don't want to work too hard,
    //it can be a regex that will be matched by the Router::route() method against the URI.
    public function setPattern($pattern);
    public function setController($controllerClassName);
    public function setAction($actionName);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.