1
\$\begingroup\$

I am building a small and 'simple' PHP framework to see if I can bring my PHP skills to a new level and to review the knowledge I have required during OOP classes and tutorials I've done.

Currently, I am building the Router class. I know people will try to discourage me reinventing the wheel, but since this is for learning purposes, I am quite happy to do so.

The 'problem' I am facing is that I cannot seem to implement "named routing parameters". There are frameworks out there, that have URL's such as /profile/id/22/name/test. The catch here is that they require predefined routes. I would like to avoid this as much as possible since my aim is to build a rather simple framework without too many configuration.

Also, my implementation seems a little bit "too basic and easy" compared to other advanced Routing mechanisms that some frameworks use. Am I missing some essentials?

So, how can my implementation/routing be improved when looking at scalability/usability and security while still keeping the simplicity?

class Router
{
    private $segments = array();
    private $parameters = array();

    /*
     * When a new Router instance is created,
     * fill it with information of the visited route
     */
    public function __construct()
    {
        $this->getSegments();
        $this->getParameters();
    }

    /*
     * Get the current requested URL
     */
    private function getURI()
    {
        return rtrim(substr($_SERVER['REQUEST_URI'], 1), '/');
    }

    /*
     * Store the route segments (controller, method, parameters)
     */
    private function getSegments()
    {
        $uri = $this->getURI();
        $this->segments = explode('/', $uri);
    }

    /*
     * Return the name of the controller
     * Returns 'index' if there is no controller given
     */
    public function getController()
    {
        return (isset($this->segments[0])) ? $this->segments[0] : 'index';
    }
    /*
     * Return the name of the method
     * Returns 'index' if there is no method given
     */
    public function getMethod()
    {
        return (isset($this->segments[1])) ? $this->segments[1] : 'index';
    }

    /*
     * Store all the given parameters
     */
    private function getParameters()
    {
        if(is_array($this->segments))
        {
            $parameters = (count($this->segments) > 2) ? array_slice($this->segments, 2) : false;

            if(!$parameters) { return false; }

            // remove empty parameters
            $parameters = array_diff($parameters, array(''));

            // reindex the array
            $parameters = array_values($parameters);

            $this->parameters = $parameters;

        }

    }

    /*
     * Return a parameter by the index
     */
    public function getParameter($index)
    {
        return (is_array($this->parameters) && isset($this->parameters[$index])) ? $this->parameters[$index] : false;
    }

    /*
     * Return all parameters
     */
    public function getAllParameters()
    {
        return (!empty($this->parameters)) ? $this->parameters : false;
    }

}

This can then be used like this

// url = {website.com}/profile/edit/22
$router = new Router();
$router->getController(); // profile
$router->getMethod(); // edit
$router->getAllParameters(); // returns all parameters
$router->getParameter(0); // 22

Note: I haven't figured out the way I want the MVC design pattern to work in my framework, so this code won't cover that.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

I am personally a fan of pretty much limiting the "routing" aspect of a front-controller like this to just getting the request to the proper controller for further processing.

But before talking about that, I want to ask you whether what you really have here is a "router" or a "URI parser"? It seems like just a URL parser to me in that is does no routing. If your goal is to create a one-fits-all URI parser, you are probably engaging in a fool's errand. As you change your application/API's behaviors and endpoint signatures, you are going to continually add more complexity to this central parser. This is probably a bad path.

Now let's think about a true "router" (this thinking exercise ideally getting towards addressing your problem with not understanding how to implement MVC). What is a router intended to do? It is intended to interpret something like a URI string and do nothing other than to route that string (and of course any payload passed to POST/PUT) to an appropriate piece of logic that knows what to do with it.

The RESTful URI structure you present really only requires one level of routing. That can be as simple as looking at first path segment of the URI, comparing against configuration mapping controllers with each "first segment" value, then instantiating the controller and passing it the remaining URI so it can determine what to do to with it from there. You might find yourself using a combination of classes such as

Router (a class to look a "first segment" and route configuration and instantiate appropriate controller and relenquish control to that controller)
RouterConfig (a class to wrap router config settings)
Controller (an abstract class all other controllers can extend from)
*Controller (specific controller implementations for each of your resources)
Request (a class to store info on request perhaps built from parse_url or similar)

Update

I also should have noted that you currently are not doing any validation on your input here. You are working directly with $_SERVER['REQUEST_URI'] which must be considered as user-input data and thus unsafe. Most good front controller implementations will have some sort of class that is instantiated to represent the request being made. This gives you an opportunity to validate/sanitize the input as well as establish a common authoritative representation of the request - the request type (GET, POST, PUT, DELETE), content type, URI, POST/PUT payload and/or parametric data, etc. - that can be passed about in the system.

I have added a more detailed code example below for what a router might look like in MVC system. This is is pretty much close to production level code other than the need to add namespacing, document blocks, and (ideally) unit tests (and of course fill in missing logic pieces).

Notice in this code how we strive to always pass around objects that we can type-hint for in the method signature. This largely eliminates the need to have parameter validation in your methods if you are disciplined about only letting objects be used in the system once they have been put into expected state (with exception being thrown otherwise.) You can see this allows us to really be efficient with the code we write.

// this is example of static router
class Router
{
    // store global route config
    protected static $config;

    // restrict this class to static usage
    private function __construct() {}

    // setter for config, likely to be called in bootstrap file of some sort
    public static function setConfig(RouterConfig $config) {
        self::$config = $config;
    }

    // main method to instantiate a controller based on route config
    public static function route(Request $request) {
        $controllerClass = self::getControllerClass($request);
        // some form of object instantiation
        return new {$controllerClass}($request);
    }

    public static function getControllerClass(Request $request) {
        self::validateConfig();
        $resourceName = $request->getUriPathSegment(0);
        // have getClassForResource return default 404 class name if match not found
        $className = {self::$config}::getClassForResource($resourceName);

        // validate that name returned maps to an existing class
        if(!class_exists($className )) {
            throw new Exception("Class: '" . $className . " not found");
        }

        return $className;
    }

    private static function validateConfig {
        if (self::config instanceof RouterConfig) return;
        throw new Exception(
            'Router::config has not bet set! You must call Router::setConfig() before calling this method.'
        );
    }            
}

// example usage

// in bootstrap/config file
Router::setConfig(/* RouterConfig object */);

// in main routing script
try {
    // some form of Request object instantiation
    $request = new Request();
    $controller = Router::route($request);
    $controller->execute();
    exit(0);
} catch (Exception $e) {
    // Perhaps log exception.
    // Perhaps handle end user messaging by instantiating 500-series controller
    // or don't catch at all and just use top-level exception handler
    exit(1);
}
\$\endgroup\$
7
  • \$\begingroup\$ Thank you very much for this neat answer. I shall try to update my project. I didnt realise that what I've built is a URI parser, but it makes sense now. Once again, thank you! \$\endgroup\$ Jan 10, 2017 at 7:32
  • \$\begingroup\$ @GerritLuimstra I have added some additional thoughts. \$\endgroup\$
    – Mike Brant
    Jan 10, 2017 at 15:04
  • \$\begingroup\$ to be completely honest here... I don't really understand what you have built here. For starters.. Why would you want to make the Router static? Purely to avoid the instantiation of this Class? Secondly, You somehow have a Controller now.. but what have you done with the method? Is that something that shouldn't have been there in the first place? I can also still not see where you implement the different types of requests with their respective types.. I am starting to understand that what I currently have is not something meant for production. \$\endgroup\$ Jan 10, 2017 at 17:35
  • \$\begingroup\$ I like the idea of seperating all the logic into different Classes, like Requests, Config, Controllers. With this I have more control of the flow of my framework. Also, you mention that $_SERVER['REQUEST_URI']is not safe. How can this be solved? Only by the methods you have provided? Sorry for any stupid questions here. I am pretty new to OOP as a whole and I simply cannot wrap my head around some concepts of a framework. \$\endgroup\$ Jan 10, 2017 at 17:38
  • \$\begingroup\$ @GerritLuimstra I was intending this code example to be a stylistic starting point not a complete implementation. For example, you might implement a Request class that, upon instantiation, parses the URI, sets other request info on the object. That way you validation of the potentially unsafe $_SERVER variables are handled in one place. I made the router static because, as a front-controller, you have no reason to instantiate concrete representations of the class, there will always be one and only one front-controller configuration at play in the system. \$\endgroup\$
    – Mike Brant
    Jan 10, 2017 at 17:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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