Add Flask-style URL converters
URL converters — an easy way to parse and generate complex URLs inspired by python's Flask (and underlaying Werkzeug library) — http://werkzeug.pocoo.org/docs/0.10/routing/#custom-converters
Trivial example
Imagine that you have URL http://example.com/foo/1,2,3,4/ and matching route /foo/[:value]/
With regular match rule you you'll get string "1,2,3,4" and need to convert it to array manualy. To generate URL from array you need to join its values first.
Now you can define URL converter
class DelimitedStringConverter implements IUrlConverter {
function __construct($delimiter=',') {
$this->delimiter = $delimiter;
}
function getRegexp() {
return '[0-9a-zA-Z]+(' . $this->delimiter . '[0-9a-zA-Z]+)*';
}
function getUrl($data) {
return join($this->delimiter, $data);
}
function getValue($url) {
return explode($this->delimiter, $url);
}
}
Register it as list matching rule
$router->addUrlConverters(array(
'list' => new DelimitedStringConverter()
));
Map route /foo/[list:values]/ — and you will get array [1, 2, 3, 4]
$router->generate( 'foo', ['values' => [1, 2, 3, 4]] ); will produce "/foo/1,2,3,4/"
Real life example
URL http://example.com/filters/producer=LG,Lenovo;display_type=IPS;front_camera=2MP;cores=2,4/
Maps to "/filters/[filter_params:filters]/"
class FilterParamsConverter implements IUrlConverter {
function __construct($keys_delimiter=';', $values_delimiter=',') {
$this->keys_delimiter = $keys_delimiter;
$this->values_delimiter = $values_delimiter;
}
function getRegexp() {
return sprintf('[0-9a-zA-Z%s_=]+(%s[0-9a-zA-Z%s_=]+)*',
$this->keys_delimiter,
$this->values_delimiter,
$this->keys_delimiter);
}
/*
* in: ['producer' => ['LG', 'Lenovo'], 'display_type' => 'IPS', 'front_camera' => '2MP', 'cores' => ['2', '4']]
* out: "cores=2,4;display_type=IPS;front_camera=2MP;producer=LG,Lenovo"
*/
function getUrl($data) {
$params = array();
foreach($data as $key => $values) {
if (!is_array($values)) {
$values = array($values);
} else {
sort($values);
}
$params[] = sprintf("%s=%s", $key, join($this->values_delimiter, $values));
}
sort($params);
return join($this->keys_delimiter, $params);
}
/*
* in: "producer=LG,Lenovo;display_type=IPS;front_camera=2MP;cores=2,4"
* out: ['producer' => ['LG', 'Lenovo'], 'display_type' => 'IPS', 'front_camera' => '2MP', 'cores' => ['2', '4']]
*/
function getValue($url) {
parse_str(str_replace($this->keys_delimiter, '&', $url), $result);
foreach($result as $key => $values) {
$result[$key] = array_unique(explode($this->values_delimiter, $values));
}
return $result;
}
}
Filters param populated with array
Array (
[producer] => Array
(
[0] => LG
[1] => Lenovo
)
[display_type] => Array
(
[0] => IPS
)
[front_camera] => Array
(
[0] => 2MP
)
[cores] => Array
(
[0] => 2
[1] => 4
)
)
Upon generation keys/values are sorted to unify URLs and avoid duplicating content with URLs differing only by params order.
Perfomance
There's no perfomance impact on routes w/o URL converter. Perfomance of routes with URL converter depends completely on getUrl(), getValue() implementation details. "Null" URL converter perfomance indistinguishable from regular route.
Why not just extend AltoRouter class?
While changes are not so big (~40 LOC), they affects all main methods — map(), generate() and match(), so you basically need to override entire logic on every AltoRouter update.
Backward incompatible changes
There are no incompatible changes in current API. Added one method addUrlConverters().
Issues/TBD
- Currently attempt to add URL converter to unnamed route throws an exception b.c. I'm using "route_name:param_name" as dict key. We can of course use route itself instead of route name, but it's a bit ugly :) Consider adding random/hashed names to unnamed routes upon map() call.
- Converter cannot reject match. Flask have special ValidationError exception — you can throw it inside URL converter (for example if matched with regexp value is out of range) and router won't match this URL.
:+1:
A bit late to the party. But, I like the functionality, although I don't think this is really the responsibility of the router, and I also think this is perfectly doable by extending AltoRouter;
Just override match and generate, call the parent and apply your functionality.