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

Added named routes, URI generator and named variables with PCRE patterns #162

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ Examples:
- `/movies/{id}`
- `/profile/{username}`

Placeholders are easier to use than PRCEs, but offer you less control as they internally get translated to a PRCE that matches any character (`.*`).
Placeholders are easier to use than PRCEs. They offer you less control as they internally get translated to a PRCE that matches any character (`.*`), unless they are used with extra PCRE patterns:

- `/movies/{id:\d+}`
- `/profile/{username:\w{3,}}`

```php
$router->get('/movies/{movieId}/photos/{photoId}', function($movieId, $photoId) {
Expand All @@ -194,6 +197,48 @@ $router->get('/movies/{foo}/photos/{bar}', function($movieId, $photoId) {
});
```

This type of placeholders is also required when using __URI Generation__ and __Named Routes__.


### Named Routes

Routes can be named, optionally. This is required when using __URI Generation__.

```php
$router->get('/movies/{movieId:\d+}/photos/', function($movieId) {
echo 'Photos of movie #' . $movieId;
}, 'movie.photos');
```


### URI Generation

Route URIs can be generated from __Named Routes__ with __Dynamic Placeholder-based Route Patterns__.

```php
$router->route('movie.photos', ['movieId' => 4711]); // Result: /movies/4711/photos
```

The array index has to match the name of the placeholder. Left-overs in the array will be added as query string:
```php
$router->route('movie.photos', ['movieId' => 4711, 'sortBy' => 'date']); // Result: /movies/4711/photos?sortBy=date
```

Note: Any PCRE-based patterns in the route will be removed.
```php
$router->get('/movies/{movieId:\d+}/photos/(\d+)?', function($movieId, $photoId=null) {
if ($photoId !== null) {
echo 'Photo #' . $photoId . ' of movie #' . $movieId;
} else {
echo 'All photos of movie #' . $movieId;
}
}, 'movie.photos');

// ...

$router->route('movie.photos', ['movieId' => 4711]); // Result: /movies/4711/photos
```


### Optional Route Subpatterns

Expand Down
92 changes: 76 additions & 16 deletions src/Bramus/Router/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class Router
*/
private $beforeRoutes = array();

/**
* @var array All named routes with their patterns
*/
private $namedRoutes = array();

/**
* @var array [object|callable] The function to be executed when no route has been matched
*/
Expand Down Expand Up @@ -74,11 +79,15 @@ public function before($methods, $pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function match($methods, $pattern, $fn)
public function match($methods, $pattern, $fn, $name=null)
{
$pattern = $this->baseRoute . '/' . trim($pattern, '/');
$pattern = $this->baseRoute ? rtrim($pattern, '/') : $pattern;

if ($name !== null) {
$this->namedRoutes[$name] = $pattern;
}

foreach (explode('|', $methods) as $method) {
$this->afterRoutes[$method][] = array(
'pattern' => $pattern,
Expand All @@ -93,9 +102,9 @@ public function match($methods, $pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function all($pattern, $fn)
public function all($pattern, $fn, $name=null)
{
$this->match('GET|POST|PUT|DELETE|OPTIONS|PATCH|HEAD', $pattern, $fn);
$this->match('GET|POST|PUT|DELETE|OPTIONS|PATCH|HEAD', $pattern, $fn, $name);
}

/**
Expand All @@ -104,9 +113,9 @@ public function all($pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function get($pattern, $fn)
public function get($pattern, $fn, $name=null)
{
$this->match('GET', $pattern, $fn);
$this->match('GET', $pattern, $fn, $name);
}

/**
Expand All @@ -115,9 +124,9 @@ public function get($pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function post($pattern, $fn)
public function post($pattern, $fn, $name=null)
{
$this->match('POST', $pattern, $fn);
$this->match('POST', $pattern, $fn, $name);
}

/**
Expand All @@ -126,9 +135,9 @@ public function post($pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function patch($pattern, $fn)
public function patch($pattern, $fn, $name=null)
{
$this->match('PATCH', $pattern, $fn);
$this->match('PATCH', $pattern, $fn, $name);
}

/**
Expand All @@ -137,9 +146,9 @@ public function patch($pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function delete($pattern, $fn)
public function delete($pattern, $fn, $name=null)
{
$this->match('DELETE', $pattern, $fn);
$this->match('DELETE', $pattern, $fn, $name);
}

/**
Expand All @@ -148,9 +157,9 @@ public function delete($pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function put($pattern, $fn)
public function put($pattern, $fn, $name=null)
{
$this->match('PUT', $pattern, $fn);
$this->match('PUT', $pattern, $fn, $name);
}

/**
Expand All @@ -159,9 +168,9 @@ public function put($pattern, $fn)
* @param string $pattern A route pattern such as /about/system
* @param object|callable $fn The handling function to be executed
*/
public function options($pattern, $fn)
public function options($pattern, $fn, $name=null)
{
$this->match('OPTIONS', $pattern, $fn);
$this->match('OPTIONS', $pattern, $fn, $name);
}

/**
Expand Down Expand Up @@ -388,8 +397,13 @@ public function trigger404($match = null){
*/
private function patternMatches($pattern, $uri, &$matches, $flags)
{
// Replace all curly braces matches {} into word patterns (like Laravel)
// Replace all curly braces matches {} into word patterns (like Laravel).
// Therefore mask quantifiers like {m,n} or {n} ... with [[m,n]] or [[n]],
// replace curly braces and then unmask quantifiers.
$pattern = preg_replace('/\{([0-9,]*)\}/', '[[\\1]]', $pattern);
$pattern = preg_replace('/\/{.*?:(.*?)}/', '/(\\1)', $pattern);
$pattern = preg_replace('/\/{(.*?)}/', '/(.*?)', $pattern);
$pattern = preg_replace('/\[\[([0-9,]*)\]\]/', '{\\1}', $pattern);

// we may have a match!
return boolval(preg_match_all('#^' . $pattern . '$#', $uri, $matches, PREG_OFFSET_CAPTURE));
Expand Down Expand Up @@ -532,4 +546,50 @@ public function setBasePath($serverBasePath)
{
$this->serverBasePath = $serverBasePath;
}

public function route($name, $vars=[]) {
if (!array_key_exists($name, $this->namedRoutes)) {
throw new \InvalidArgumentException(sprintf('Named route %s does not exist!', $name));
}

$route = $this->namedRoutes[$name];

return $this->generateUri($route, $vars);
}

public function generateUri($route, $vars=[]) {
// remove positional, optional placeholders from route uri
do {
$route = preg_replace('/\([^(]*\)\?/', '', $route, -1, $count);
} while ($count > 0);

// remove all quantifiers b/c of colliding curly braces
$route = preg_replace('/\{[0-9,]*\}/', '', $route);

// replace named variables like /user/{username}
$route = preg_replace_callback_array([
'/(?|\{([^}:]+?)\}|\{([^:]+):.+?\})/' => function ($match) use ($route, &$vars) {
$varname = $match[1];

if (array_key_exists($varname, $vars)) {
$value = $vars[$varname];
unset($vars[$varname]);

return $value;
}

throw new \InvalidArgumentException(
sprintf('Replacement for mandatory variable %s is missing in route %s!', $varname, $route));

return "";
},
], $route);

// build query string from left variables
if (!empty($vars)) {
$route .= '?' . http_build_query($vars);
}

return $this->getBasePath() . ltrim($route, '/');
}
}