From f01398bb14c69d1633c63645f2f19b4597aa23c1 Mon Sep 17 00:00:00 2001 From: Erik Schindler Date: Sun, 15 Aug 2021 01:57:53 +0200 Subject: [PATCH 1/3] Added PCRE patterns for named placeholders, named routes and route URI generation. --- README.md | 47 ++++++++++++++++++++- src/Bramus/Router/Router.php | 82 +++++++++++++++++++++++++++++------- 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b0f61a5..060c606 100755 --- a/README.md +++ b/README.md @@ -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) { @@ -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 diff --git a/src/Bramus/Router/Router.php b/src/Bramus/Router/Router.php index a8f206a..aa01b92 100644 --- a/src/Bramus/Router/Router.php +++ b/src/Bramus/Router/Router.php @@ -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 */ @@ -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, @@ -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); } /** @@ -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); } /** @@ -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); } /** @@ -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); } /** @@ -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); } /** @@ -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); } /** @@ -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); } /** @@ -532,4 +541,47 @@ 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 placeholders from route uri + do { + $route = preg_replace('/\([^(]*\)\??/', '', $route, -1, $count); + } while ($count > 0); + + // 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 $route; + } } From 20f04d6e1e5cbab51bc86d46f475af3b40ef4cb8 Mon Sep 17 00:00:00 2001 From: Erik Schindler Date: Sun, 15 Aug 2021 14:06:22 +0200 Subject: [PATCH 2/3] Fixed curly braces collision of named variables and quantifiers --- src/Bramus/Router/Router.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Bramus/Router/Router.php b/src/Bramus/Router/Router.php index aa01b92..83f2500 100644 --- a/src/Bramus/Router/Router.php +++ b/src/Bramus/Router/Router.php @@ -397,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)); @@ -553,14 +558,17 @@ public function route($name, $vars=[]) { } public function generateUri($route, $vars=[]) { - // remove positional placeholders from route uri + // remove positional, optional placeholders from route uri do { - $route = preg_replace('/\([^(]*\)\??/', '', $route, -1, $count); + $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) { + '/(?|\{([^}:]+?)\}|\{([^:]+):.+?\})/' => function ($match) use ($route, &$vars) { $varname = $match[1]; if (array_key_exists($varname, $vars)) { From d4f9a42dff4f9a12c78bec56325ebed9f7d726dd Mon Sep 17 00:00:00 2001 From: mintalicious Date: Mon, 30 Aug 2021 18:04:34 +0200 Subject: [PATCH 3/3] Added base path to generated route --- src/Bramus/Router/Router.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bramus/Router/Router.php b/src/Bramus/Router/Router.php index 83f2500..32a3403 100644 --- a/src/Bramus/Router/Router.php +++ b/src/Bramus/Router/Router.php @@ -590,6 +590,6 @@ public function generateUri($route, $vars=[]) { $route .= '?' . http_build_query($vars); } - return $route; + return $this->getBasePath() . ltrim($route, '/'); } }