Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route to URL generation / reverse routing #66

Closed
anlutro opened this issue Aug 12, 2015 · 8 comments · Fixed by #277
Closed

Route to URL generation / reverse routing #66

anlutro opened this issue Aug 12, 2015 · 8 comments · Fixed by #277

Comments

@anlutro
Copy link
Contributor

anlutro commented Aug 12, 2015

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 31fa869) 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?

@nikic
Copy link
Owner

nikic commented Aug 12, 2015

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.

@anlutro
Copy link
Contributor Author

anlutro commented Aug 15, 2015

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.

@fredemmott
Copy link
Contributor

I'd appreciate this too :)

@sitilge
Copy link

sitilge commented Dec 28, 2015

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)?

@nikic
Copy link
Owner

nikic commented Dec 28, 2015

@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.

@jdv145 jdv145 mentioned this issue Aug 4, 2017
@nikic nikic changed the title Route to URL generation, preg_replace_callback regex constant Route to URL generation / reverse routing Sep 17, 2017
@lackovic10
Copy link

@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.

@tobiasmuehl
Copy link

@lackovic10 Please share

@khalyomede
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants