Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Backbone-like router with dynamic regex in PHP

I've already written a PHP controller, but I'm rewriting my code so I can have Backbone Router-like JSON routes that map URI patterns to PHP Class::method combinations, or directly to HTML documents which are then delivered to the client, like so:

{
  "/home" : "index.html",
  "/podcasts": "podcasts.html",
  "/podcasts/:param1/:param2": "SomeClass::someMethod"
}

Backbone dynamically creates regexes to match routes against URLs. I took a look into the backbone code, and I extracted the following code (it's a bit modified):

function _routeToRegExp (route) {
  var optionalParam = /\((.*?)\)/g;
  var namedParam    = /(\(\?)?:\w+/g;
  var splatParam    = /\*\w+/g;
  var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;
  route = route.replace(escapeRegExp, '\\$&')
               .replace(optionalParam, '(?:$1)?')
               .replace(namedParam, function(match, optional){
                 return optional ? match : '([^\/]+)';
               })
              .replace(splatParam, '(.*?)');
  return new RegExp('^' + route + '$');
}

When I pass a route like /podcasts/:param1/:param2, to the code above, I get /^\/podcasts\/([^\/]+)\/([^\/]+)$/. I was trying to write a PHP function to get exactly that same regex. I tried:

$route = '/podcasts/:param1/:param2';
$a = preg_replace('/[\-{}\[\]+?.,\\\^$|#\s]/', '\\$&', $route); // escapeRegExp
$b = preg_replace('/\((.*?)\)/', '(?:$1)?', $a);                // optionalParam
$c = preg_replace('/(\(\?)?:\w+/', '([^\/]+)', $b);             // namedParam
$d = preg_replace('/\*\w+/', '(.*?)', $c);                      // splatParam
$pattern = "/^{$d}$/";
echo "/^\/podcasts\/([^\/]+)\/([^\/]+)$/\n";
echo "{$pattern}\n";
$matches = array();
preg_match_all($pattern, '/podcasts/param1/param2', $matches);
print_r($matches);

My output is:

/^\/podcasts\/([^\/]+)\/([^\/]+)$/    // The expected pattern
/^/podcasts/([^\/]+)/([^\/]+)$/       // echo "{$pattern}\n";
Array                                 // print_r($matches);
(
)

Why is my regex output different? I can handle the rest of the mapping process and all, but I haven't figured out how to obtain exactly the same regex in PHP as in Javascript. Any suggestions?

like image 748
Óscar Palacios Avatar asked Jan 31 '13 17:01

Óscar Palacios


1 Answers

The JS version does not in fact escape the / chars, it produces the same string representation as your PHP version /podcasts/([^/]+)/([^/]+) but, and that's probably what tripped you, a console.log of the regexp returned by _routeToRegExp shows the full representation with the delimiting / and the inner ones escaped :

//Firefox output
RegExp /^\/podcasts\/([^\/]+)\/([^\/]+)$/ 

See http://jsfiddle.net/w6zP2/ for a demo.

What @ajshort noted in the comments and what Backbone does are the same solution : skip the / escaping and use an alternative syntax; Backbone does it with an explicit call to the Regexp constructor with new RegExp('^' + route + '$'), you can do something similar with $pattern = "~^{$d}$~";

And a PHP Fiddle http://phpfiddle.org/main/code/4de-jf3 that seems to work as expected.

Note that in Javascript $& in a replacement string inserts the matched substring1, not so in PHP2. The escapeRegExp should be modified and might look like :

$a = preg_replace('/[\-{}\[\]+?.,\\\^$|#~\s]/', '\\\\$0', $route);

which gives us an updated code:

$a = preg_replace('/[\-{}\[\]+?.,\\\^$|#~\s]/', '\\\\$0', $route); // escapeRegExp
$b = preg_replace('/\((.*?)\)/', '(?:$1)?', $a);                // optionalParam
$c = preg_replace('/(\(\?)?:\w+/', '([^\/]+)', $b);             // namedParam
$d = preg_replace('/\*\w+/', '(.*?)', $c);                      // splatParam
$pattern = "~^{$d}$~";

echo "/^\/podcasts\/([^\/]+)\/([^\/]+)$/\n";
echo "{$pattern}\n";
$matches = array();
preg_match_all($pattern, '/podcasts/param1/param2', $matches);
print_r($matches);

1 See Specifying a string as a parameter in string.replace
2 See replacement in preg_replace

like image 164
nikoshr Avatar answered Oct 15 '22 22:10

nikoshr