8

I am writing a class that handles routing of my PHP webservice but I need to correct the regex, and I want to know what would be the most effecient way to parse the url.

Example urls:

  • POST /users
  • GET /users
  • GET /users&limit=10&offset=0
  • GET /users/search&keyword=Richard
  • GET /users/15/posts/38

What I want to create in PHP for class is this:

$router = new Router();
$router->addRoute('POST', '/users', function(){});
$router->addRoute('GET', '/users/:uid/posts/:pid', function($uid, $pid){});
$target = $router->doRouting();

The target variable would now contain an array with:

  • method
  • url
  • callback method

This is what I got so far:

class Router{
    use Singleton;

    private $routes = [];
    private $routeCount = 0;

    public function addRoute($method, $url, $callback){
        $this->routes[] = ['method' => $method, 'url' => $url, 'callback' => $callback];
        $this->routeCount++;
    }

    public function doRouting(){
        $reqUrl = $_SERVER['REQUEST_URI'];
        $reqMet = $_SERVER['REQUEST_METHOD'];

        for($i = 0; $i < $this->routeCount; $i++){
            // Check if the url matches ...
            // Parse the arguments of the url ...
        }
    }
}

So I need a regex that first of all:

  1. /mainAction/:argumentName/secondaryAction/:secondaryActionName

checks if that matches the $reqUrl (see at the for loop above)

  1. Extracts the arguments, so we can use them in our callback function.

What I tried myself:

(code should be in the for loop @ doRouting function)

// Extract arguments ...
$this->routing[$i]['url'] = str_replace(':arg', '.+', $this->routing[$i]['url']);

// Does the url matches the routing url?
if(preg_match('#^' . $this->routes[$i]['url'] . '$#', $reqUrl)){
    return $this->routes[$i];
}

I really appreciate all help, thanks alot.

2
  • This should work if you can frame your arguments like GET php.net/manual/en/function.parse-url.php Commented Jul 30, 2012 at 13:38
  • @fdomig true, I added what I tried myself. I am not lazy, I'm just not good at regexes and stuck at it haha :P Commented Jul 30, 2012 at 13:49

2 Answers 2

12

this basicly works now.

public function doRouting(){
    // I used PATH_INFO instead of REQUEST_URI, because the 
    // application may not be in the root direcory
    // and we dont want stuff like ?var=value
    $reqUrl = $_SERVER['PATH_INFO'];
    $reqMet = $_SERVER['REQUEST_METHOD'];

    foreach($this->routes as  $route) {
        // convert urls like '/users/:uid/posts/:pid' to regular expression
        $pattern = "@^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['url'])) . "$@D";
        $matches = Array();
        // check if the current request matches the expression
        if($reqMet == $route['method'] && preg_match($pattern, $reqUrl, $matches)) {
            // remove the first match
            array_shift($matches);
            // call the callback with the matched positions as params
            return call_user_func_array($route['callback'], $matches);
        }
    }
}

PS: You dont need the $routeCount attribute

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks alot, however there is one problem with your regular expression: "/users/:uid/" would route to the same as "/users/:uid/messages/:mid", so we got to check somewhere if it is fully the same not partly in the regex?
Great answer. Cleanest PHP router I came across. Did a small modification to support regular expression. Not sure why you use preq_quote ?
1

Great answer @MarcDefiant. Cleanest PHP router I came across. Did a small modification to support regular expression as well. Not sure why you use preq_quote ?

Small todo would be to cast the array to a assoc array. E.g. replace ['0' => 1] with ['id' => 1]

function matchRoute($routes = [], $url = null, $method = 'GET')
{
    // I used PATH_INFO instead of REQUEST_URI, because the 
    // application may not be in the root direcory
    // and we dont want stuff like ?var=value
    $reqUrl = $url ?? $_SERVER['PATH_INFO'];
    $reqMet = $method ?? $_SERVER['REQUEST_METHOD'];

    $reqUrl = rtrim($reqUrl,"/");

    foreach ($routes as $route) {
        // convert urls like '/users/:uid/posts/:pid' to regular expression
        // $pattern = "@^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($route['url'])) . "$@D";
        $pattern = "@^" . preg_replace('/:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', $route['url']) . "$@D";
        // echo $pattern."\n";
        $params = [];
        // check if the current request params the expression
        $match = preg_match($pattern, $reqUrl, $params);
        if ($reqMet == $route['method'] && $match) {
            // remove the first match
            array_shift($params);
            // call the callback with the matched positions as params
            // return call_user_func_array($route['callback'], $params);
            return [$route, $params];
        }
    }
    return [];
}


$match = matchRoute([
    [
        'method' => 'GET',
        'url' => '/:id',
        'callback' => function($req) {
            exit('Hello');
        }
    ],
    [
        'method' => 'GET',
        'url' => '/api/(.*)', // Match all /api/hello/test/...
        'callback' => function($req) {
            print_r($req);
            exit('cool');
        }
    ]
]);

list($route,$params) = $match;

call_user_func_array($route['callback'], [$params]);

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.