Router/Dispatch Design Issue

Hi guys, it’s been a long while since I’ve been to these forums but I’m in a bit of a predicament at the moment trying to resolve an issue of who is responsible for what (between my Router & Dispatcher).

I have a working Router implemented which takes a series of Route patterns and matches them against a given URL. The first to match successfully will have my Router return a Command object full of all the goodies my Dispatcher needs (name of controller, action and any other parameters).

At the moment this Command object would be passed to the Dispatcher to initiate the controller and dispatch the action.

It is however right at this same point that I am a little stuck with how to handle things.

I would very much like to have the ability (as most frameworks do) to default back to a default Module, Controller & Action if the Route returned by my Router is not dispatchable as is.

Here is where the problem lies. What (between the Router & the Dispatcher) do you guys think should be responsible for injecting these defaults?

On one had I have a Router which in my eyes should simply match patterns to urls and hand control to something else when it’s done. On the other hand, I don’t want the Dispatcher to be playing around with what is (at this stage) considered a valid Route.

A big part of me want’s this to be handled at the Router. I already have plans for another step in the bootstrap process to sit between the Router and Dispatcher but it (this other step) will definitely require knowledge of what is about to be dispatched. However allot of code that I have seen seems to have the Dispatcher attempt to load what the Router has given it, and if it can’t the dispatcher itself starts injecting defaults until it can.

Any thoughts on the subject would be much appreciated. Thanks.

ps: I have the project hosted on Github if seeing code might make things clearer. I just didn’t want to link it here as I haven’t posted in a while and didn’t want to give the impression I was spamming.

I’ve been playing with my own framework since using CodeIgniter, which has helped a great deal.

Using regex, I replace the current route with the new route and values…

i.e.


    $config['source'] = 'QUERY_STRING';
    $config['default_controller'] = 'common';
    $config['default_action'] = 'index';

    $routes['sample/route/(:num)/(:any)'] = 'converted/to/$1/$2/$1';

class RouteHandler
    {
        private $Routes = array();
        public function __construct()
        {
            // load in the route configuration
            ConfigurationHandler::$Instance->LoadConfig('route');
            
            // although we have already loaded the route
            // configuration file, we need the $routes array
            // from it. We'll find a better way to handle this
            // soon.
            if(file_exists(APP . 'config' . DS . 'route.php'))
            {
                require APP . 'config' . DS . 'route.php';
                if(isset($routes))
                {
                    $this->Routes = $routes;
                }
            }
        }

        public function Route($path = null)
        {
            // if the path we send across is null
            // get it from the URL
            if(is_null($path))
            {
                // get the key to use against $_SERVER
                // from the configuration
                $Source = ConfigurationHandler::$Instance->GetItem('source', 'QUERY_STRING', 'route');
                // get the value from $_SERVER using the key
                $path = (isset($_SERVER[$Source]) ? $_SERVER[$Source] : '');
            }
            // prepare the route
            $path = $this->prepareRoute(strpos($path, 'path=') == 0 ? substr($path, 5) : $path);
            $parts = explode('/', $path);

            // get the controller from the path, 
            // or default to a value found in the config
            $controller = ucfirst((empty($parts) || empty($parts[0]) ? ConfigurationHandler::$Instance->GetItem('default_controller', '', 'route') : array_shift($parts)));
            // get the action from the path,
            // or default to a value found in the config
            $action = (empty($parts) || empty($parts[0]) ? ConfigurationHandler::$Instance->GetItem('default_action', 'index', 'route') : array_shift($parts));
            // whats left are the parameters
            $params = $parts;

            // check to see if the controller exists
            if(file_exists(APP . 'controllers' . DS . $controller . '.php'))
            {
                require_once(APP . 'controllers' . DS . $controller . '.php');
            }
            else
            {
                die('Unable to locate the required controller: ' . $controller);
            }
            // create a instance of the controller
            $ControllerInstance = new $controller();
            // check to see if the action exists in the controller
            if(!method_exists($ControllerInstance, $action))
            {
                die('Unable to find the required action: ' . $action . ' within the controller: ' . $controller);
            }
            // call the action inside the controller
            call_user_func_array(array($ControllerInstance, $action), $params);
        }

        private function prepareRoute($Path)
        {
            // go through each of our results
            foreach ($this->Routes as $key => $val)
            {
                // replace our friendly code with Regex code
                $key = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $key));                
                // if it matches
                if (preg_match('#^'.$key.'$#', $Path))
                {
                    // replace the current url with the routed url
                    if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
                    {
                        $val = preg_replace('#^'.$key.'$#', $val, $Path);
                    }
                    return $val;
                }
            }
            return $Path;
        }
    }

Hope this helps you?

Cool, but that doesn’t answer the question does it.

Do you need a command object? Is it anything more than a data structure? Does it actually have behavior?

Ideally you’ll want to be able to call your dispatcher from multiple locations in the application. Removing the dependency of a ‘command’ object and just passing string parameters makes this far easier.

Back on topic:

In short,

The router should only be concerned with URL matching. It shouldn’t know anything about whether specific controllers exist or what’s going to happen beyond that point.

e.g. it will take an input of

/foo/bar/1/2/3

and return something like


array(
	'controller' => 'foo',
	'action' => 'bar',
	'params' => array(1, 2, 3)
);

This, I’m assuming is what you’re storing in your command object. I see no reason the router wouldn’t have the ability to check whether controllers exist to work out whether to route to the default controller.

The dispatcher would take a set of as above, potentially controller, action and parameters and call the specified controller.

You may want to look at these posts:

Have gone for the router returns a closure approach.


class Router
{
    private $routing;
    private $actions;

    function __construct()
    {
        $this->routing = new SplObjectStorage();
        $this->actions = new SplObjectStorage();
    }
    
    /*
    	Add a route to the router.
    	
    	Internally creates a tree or collection of trees/forest
    	
    	The children of a given node are only visited if the parent node returns true (ie some match)
    
    	$matches - Array of closures that return true or false
    	$action - Closure to return if all of the closures in $matches return true
    */
    function add(array $matches, Closure $action)
    {
        $routing = $this->routing;
        foreach($matches as $match)
        {
            if (!$routing->contains($match))
                $routing->attach($match, new SplObjectStorage());
            $routing = $routing[$match];
        }
        $this->actions->attach($routing, $action);
        return $this;
    }
    
    private function lookup(SplObjectStorage $routing, Context $context)
    {
        foreach($routing as $accept)
            if ($accept($context))
            {
            	// Parent condition returned true, descend into children
                $action = $this->lookup($routing[$accept], $context);
                if ($action)
                    return $action;
            }
        // Lookup related action given routing..
        return $this->actions->contains($routing) ? $this->actions[$routing] : null;
    }

    function lookupAction(Context $context)
    {
        return $this->lookup($this->routing, $context);
    }
}

The method of setting the default action is just calling the add() method with an empty array as $matches.


$router = new Router();
$router->add(array(), function() { echo 'Default action'; });

And this is located together with the rest of the router configuration.