FastRoute
FastRoute copied to clipboard
Route to URL generation / reverse routing
Previously, I used FastRoute\RouteParser\Std::VARIABLE_REGEX
with preg_replace_callback
in order to make a FastRoute-compatible route URL generator, but in 0.6 (more specifically https://github.com/nikic/FastRoute/commit/31fa86924556b80735f98b294a7ffdfb26789f22) this was changed.
Obviously my tests picked it up and I fixed it by wrapping the regex string in ~
and ~x
, but I'm now uncertain if that's the correct approach - I guess it won't work properly with the new trailing optional segments.
Could a preg_replace_callback
compatible regex, including necessary flags, be part of the public API for FastRoute? Or, taking it a step further, could a "FastRoute-pattern to URL generator" class/function be part of the package?
Instead of trying to directly use the regex (which, as you say, doesn't work with options in any case -- this is not described by a single regex) I'd suggest working on the parsed result instead. Something similar to this should do:
public function getPath(array $params = array()) {
$routeParser = new RouteParser\Std;
// (Maybe store the parsed form directly)
$routes = $routeParser->parse($this->pattern);
// One route pattern can correspond to multiple routes if it has optional parts
foreach ($routes as $route) {
$url = '';
$paramIdx = 0;
foreach ($route as $part) {
// Fixed segment in the route
if (is_string($part)) {
$url .= $part;
continue;
}
// Placeholder in the route
if ($paramIdx === count($params)) {
throw new LogicException('Not enough parameters given');
}
$url .= $params[$paramIdx++];
}
// If number of params in route matches with number of params given, use that route.
// Otherwise try to find a route that has more params
if ($paramIdx === count($params)) {
return $url;
}
}
throw new LogicException('Too many parameters given');
}
Note that with optionals there can be ambiguity as to which URL should be generated. E.g. with /users[/{id:\d+}]
it is possible to distinguish between /users
and /users/{id}
based on the number of parameters passed to the URLification function. With /users[/foo]
on the other hand it's not possible to distinguish between them based on param count (the above code will choose the shorter URL).
If you don't care about optionals, then your code should work fine, or alternative the inner loop of the above snippet should be enough.
That seems quite easy, I'll give it a shot.
What do you think of the idea to make such a function part of this library's API? It can't be a very uncommon use-case.
I'd appreciate this too :)
Agree with @anlutro and @fredemmott .
The router is really nice, just a thought here - will the fast route will stay as request router or the functionality described above could be implemented in FastRoute someday (I guess, making it a 'response router' also)?
@sitilge I wouldn't know how to integrate URL generation into the project as it currently is. We'd at least need named routes for that to reasonably work.
@nikic at chesskid.com we're using fast route with named routes, which makes route generation quite easy. if you're interested in the idea i can send code.
@lackovic10 Please share
I created an URL reconstructor, and then I saw this issue with the example code.
My version is also checking that the regular expression is matching the named parameter, and it supports providing URL parameters as named or indexed (which works if you mix those).
Throwing my 3 cents here, as I did not made the effort to see how I could open a PR for this:
private static function replaceRouteParameters(string $route, array $parameters): string
{
$routeDatas = (new Std())->parse($route);
$placeholders = $parameters;
$url = "";
foreach ($routeDatas as $routeData) {
foreach ($routeData as $data) {
if (is_string($data)) {
// This is a string, so nothing to replace inside of it
$url .= $data;
} elseif (is_array($data)) {
// This is an array, so it contains in first the name of the parameter, and in second the regular expression.
// Example, [0 => "name", 1 => "[^/]"]
[$parameterName, $regularExpression] = $data;
$parameterValue = null;
if (isset($placeholders[$parameterName])) {
// If the parameter name is found by its key in the $parameters parameter, we use it
$parameterValue = $placeholders[$parameterName];
// We remove it from the remaining placeholders values
unset($placeholders[$parameterName]);
} elseif (isset($placeholders[0])) {
// Else, we take the first parameter in the $parameters parameter
$parameterValue = $placeholders[0];
// We remove it from the remaining available placeholders values
array_shift($placeholders);
} else {
throw new InvalidArgumentException("parameter $parameterName missing for route $route");
}
// Checking if the value found matches the regular expression of the associated route parameter
$matches = [];
$success = preg_match("/" . str_replace("/", "\/", $regularExpression) . "/", (string) $parameterValue, $matches);
if ($success !== 1 || (isset($matches[0]) && $parameterValue != $matches[0])) {
throw new InvalidArgumentException("parameter $parameterName does not matches regular expression $regularExpression for route $route");
}
$url .= $parameterValue;
}
}
}
return $url;
}
It comes from a package I made to provide a standalone router, based on nikic's: folded/routing.
If you guys like the idea, I may see in the next weeks how I can integrate it in a PR for this package.