1
votes

I'm facing a situation where I don't know how to implement a function, I'm not sure what could be the best and faster solution.

I have a simple Routing object, pretty basic, I don't need advanced functionalities for this particular project... it stores an array of routes, the only methods allowed are GET and POST and this is roughly the class structure:

class Router
{
    // Array of Route Objects
    private static $binded_routes = array();

    // Method used to register a GET route.
    public static function get() {}

    // Method used to register a POST route.
    public static function post() {}

    // Other methods here like redirect(), routeTo(), dispatch()
}

Routes can be declared as this:

Router::get('index', 'IndexController@method');
Router::get('users/{id}', 'UserController@showUser');
Router::get('route/to/something', 'Controller@method');
Router::get('route/to/something/{param1}', 'Controller@method1');
Router::get('route/to/something/{param1}/{param2}', 'Controller@method2');

The policy to store GET routes is this:

  1. Only register route without params (in this example: index, users, route/to/something)
  2. Where params are specified store them as an array
  3. Do not store more than one GET route with the same amount of params (in this example declaring 'users/{test}' will throw an error)

Route object is like this:

class Route
{
    private $route_type = 'GET';
    private $route_name = null;
    private $route_uri = null;
    private $route_params = array();
    private $route_controller = null;
    private $route_method = null;

    // Functions to correctly store and retrieve the above values
}

So now I'm having trouble on matching GET requests, based on the policy I could do something like that:

  1. loop through all the binded routes. find an exact match and stop if found.
    -> So if a user go to 'route/to/something' I can match the third route and pass execution to the right controller.
  2. If not found, match as much of the route as possible and take the rest as parameters.
    -> So if a user go to 'route/to/something/1/2' I can match 'route/to/something' and have array(1,2) as parameters
  3. Now I can simply count the number of params and compare to the routes to find the only one that has the same amount of params.

At the moment I can't think of a way to manage this process without having more than one foreach loop. What could be the best approach for this? is there a way to structure a regexp? and how to generate it?

Any help would be highly appreciated, and if you need some more info just let me know.

1

1 Answers

2
votes

After a bit of coding I've managed to create a working function, the tricky part was to match GET requests with params.

For example, if I have these routes:

Router::get('user/{id}', 'UserController@showUser');
Router::get('route/path/{param1}', 'SomeController@someMethodA');
Router::get('route/path/{param1}/{param2}', 'SomeController@someMethodB');

The user can make requests through the browser like this:

site.com/user/10
site.com/route/path/10
site.com/route/path/10/20

Knowing that, my script must recognize (following the policy on how to parse GET request) the requested URIs in the following way:

route1: user
params: array(10)

route2: route/path
params: array(10)

route3: route/path
params: array(10,20)

Here is the relevant part of the code:

$index = 0;
$array_of_matches = array();

// $current_uri is urldecoded route path
$splitted_uri = explode('/', $current_uri);

foreach (self::$binded_routes as $route) 
{
    if ($route->getURI() === $current_uri && !$route->hasParams()) 
    {
        // Gotcha.
        $found_route = true;
        $route_index = $index;

        // No need to continue wasting time...
        break;
    }

    $number_of_matches = 0;
    $route_uri_split = explode('/', $route->getURI());

    if ($splitted_uri[0] == $route_uri_split[0] && $route->hasParams()) 
    {
        $number_of_matches++;

        // I need this to eliminate routes like
        // users/list when searching for users/{1}
        if (count($route_uri_split) > count($splitted_uri)) 
        {
            $number_of_matches = 0;
        }

        for($i = 1; $i < count($splitted_uri); $i++) 
        {
            if (isset($route_uri_split[$i])) 
            {
                if ($route_uri_split[$i] === $splitted_uri[$i])
                    $number_of_matches++;
                else
                    $number_of_matches--;
            }
        }
        $array_of_matches[$index] = $number_of_matches;
    }

    // Incrementing index for next array entry.
    $index ++;
}

// Now try to find the route with the same amount of params if I still don't have a match.
if (!$found_route) 
{
    $highest_matches = array_keys($array_of_matches, max($array_of_matches));
    foreach ($highest_matches as $match) 
    {
        $matched_route = self::$binded_routes[$match];
        $params_portion = ltrim(str_replace($matched_route->getURI(), '', $current_uri), '/');

        // If $params_portion is empty it means that no params are passed.
        $params_count = (empty($params_portion)) ? 0 : count(explode('/', $params_portion));

        if ($params_count == $matched_route->paramsCount()) 
        {
            $found_route = true;
            $route_index = $match;
            $route_params = explode('/', $params_portion);

            break;
        }
    }
}

if ($found_route) 
{    
    // If params are needed set them now.
    if (isset($route_params))
        self::$binded_routes[$route_index]->setParams($route_params);

    // Dispatch the route.
    self::$binded_routes[$route_index]->dispatch();
}
else 
{
    // Route not found... redirect to 404 or error.
}

Now, I know it looks pretty ugly and I would like to improve this code where possible. Apart from extracting the code to its own class context, delegating and making it more "sweet", maybe it can be made faster, more efficient or smart.

If you have some ideas please let me know.