Skip to content

Commit

Permalink
Merge pull request #196 from willy68/FastRoute_format
Browse files Browse the repository at this point in the history
FastRoute format
  • Loading branch information
harikt authored Jun 12, 2023
2 parents 8c337a3 + fe6d4db commit 52fc0be
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 67 deletions.
129 changes: 91 additions & 38 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @license http://opensource.org/licenses/bsd-license.php BSD
*
*/

namespace Aura\Router;

/**
Expand All @@ -17,6 +18,10 @@
*/
class Generator
{
const REGEX = '#{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*:*\s*([^{}]*{*[^{}]*}*[^{}]*)\s*}#';
const OPT_REGEX = '#{\s*/\s*([a-z][a-zA-Z0-9_-]*\s*:*\s*[^/]*{*[^/]*}*[^/]*,*)}#';
const EXPLODE_REGEX = '#\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*(?::*\s*([^,]*[{\d+,}]*[^,\w\s])[^}]?)?#';

/**
*
* The map of all routes.
Expand Down Expand Up @@ -115,25 +120,6 @@ public function generate($name, array $data = [])
return $this->build($name, $data, false);
}

/**
*
* Generate the route without url encoding.
*
* @param string $name The route name to look up.
*
* @param array $data The data to interpolate into the URI; data keys
* map to attribute tokens in the path.
*
* @return string A URI path string
*
* @throws Exception\RouteNotFound
*
*/
public function generateRaw($name, array $data = [])
{
return $this->build($name, $data, true);
}

/**
*
* Gets the URL for a Route.
Expand All @@ -145,10 +131,10 @@ public function generateRaw($name, array $data = [])
*
* @param bool $raw Leave the data unencoded?
*
* @throws Exception\RouteNotFound
*
* @return string
*
* @throws Exception\RouteNotFound
*
*/
protected function build($name, array $data, $raw)
{
Expand Down Expand Up @@ -178,7 +164,7 @@ protected function buildUrl()
$this->url = $this->basepath . $this->route->path;

$host = $this->route->host;
if (! $host) {
if (!$host) {
return;
}
$this->url = '//' . $host . $this->url;
Expand All @@ -200,11 +186,49 @@ protected function buildUrl()
*/
protected function buildTokenReplacements()
{
foreach ($this->data as $key => $val) {
$this->repl["{{$key}}"] = $this->encode($val);
preg_match_all(self::REGEX, $this->url, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$name = $match[1];
foreach ($this->data as $key => $val) {
if ($key === $name) {
$token = isset($match[2]) ? $match[2] : null;
if (isset($this->route->tokens[$name]) && is_string($this->route->tokens[$name])) {
// if $token is null use route token
$token = $token ?: $this->route->tokens[$name];
}
if ($token) {
if (!preg_match('~^' . $token . '$~', (string)$val)) {
throw new \RuntimeException(sprintf(
'Parameter value for [%s] did not match the regex `%s`',
$name,
$token
));
}
}
$this->repl[$match[0]] = $this->encode($val);
}
}
}
}

/**
*
* Encodes values, or leaves them raw.
*
* @param string $val The value to encode or leave raw.
*
* @return mixed
*
*/
protected function encode($val)
{
if ($this->raw) {
return $val;
}

return is_scalar($val) ? rawurlencode($val) : null;
}

/**
*
* Builds replacements for attributes in the generated path.
Expand All @@ -215,13 +239,23 @@ protected function buildTokenReplacements()
protected function buildOptionalReplacements()
{
// replacements for optional attributes, if any
preg_match('#{/([a-z][a-zA-Z0-9_,]*)}#', $this->url, $matches);
if (! $matches) {
preg_match(self::OPT_REGEX, $this->url, $matches);
if (!$matches) {
return;
}

// the optional attribute names in the token
$names = explode(',', $matches[1]);
$names = [];
preg_match_all(self::EXPLODE_REGEX, $matches[1], $exMatches, PREG_SET_ORDER);
foreach ($exMatches as $match) {
$name = $match[1];
$token = isset($match[2]) ? $match[2] : null;
if (isset($this->route->tokens[$name]) && is_string($this->route->tokens[$name])) {
// if $token is null use route token
$token = $token ?: $this->route->tokens[$name];
}
$names[] = $token ? [$name, $token] : $name;
}

// this is the full token to replace in the path
$key = $matches[0];
Expand All @@ -243,14 +277,32 @@ protected function buildOptionalReplacement($names)
{
$repl = '';
foreach ($names as $name) {
$token = null;
if (is_array($name)) {
$token = $name[1];
$name = $name[0];
}
// is there data for this optional attribute?
if (! isset($this->data[$name])) {
if (!isset($this->data[$name])) {
// options are *sequentially* optional, so if one is
// missing, we're done
return $repl;
}

$val = $this->data[$name];

// Check val matching token
if ($token) {
if (!preg_match('~^' . $token . '$~', (string)$val)) {
throw new \RuntimeException(sprintf(
'Parameter value for [%s] did not match the regex `%s`',
$name,
$token
));
}
}
// encode the optional value
$repl .= '/' . $this->encode($this->data[$name]);
$repl .= '/' . $this->encode($val);
}
return $repl;
}
Expand All @@ -275,19 +327,20 @@ protected function buildWildcardReplacement()

/**
*
* Encodes values, or leaves them raw.
* Generate the route without url encoding.
*
* @param string $val The value to encode or leave raw.
* @param string $name The route name to look up.
*
* @return mixed
* @param array $data The data to interpolate into the URI; data keys
* map to attribute tokens in the path.
*
* @return string A URI path string
*
* @throws Exception\RouteNotFound
*
*/
protected function encode($val)
public function generateRaw($name, array $data = [])
{
if ($this->raw) {
return $val;
}

return is_scalar($val) ? rawurlencode($val) : null;
return $this->build($name, $data, true);
}
}
1 change: 0 additions & 1 deletion src/Matcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/
namespace Aura\Router;

use Aura\Router\Exception;
use Aura\Router\Rule\RuleIterator;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
Expand Down
49 changes: 35 additions & 14 deletions src/Rule/Path.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
*/
class Path implements RuleInterface
{
const REGEX = '#{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*:*\s*([^{}]*{*[^{}]*}*[^{}]*)\s*}#';
const OPT_REGEX = '#{\s*/\s*([a-z][a-zA-Z0-9_-]*\s*:*\s*[^/]*{*[^/]*}*[^/]*,*)}#';
const SPLIT_REGEX = '#\s*,\s*(?![^{]*})#';

/**
*
* Use this Route to build the regex.
Expand Down Expand Up @@ -158,12 +162,10 @@ protected function buildRegex(Route $route)
* Expands optional attributes in the regex from ``{/foo,bar,baz}` to
* `(/{foo}(/{bar}(/{baz})?)?)?`.
*
* @return null
*
*/
protected function setRegexOptionalAttributes()
{
preg_match('#{/([a-z][a-zA-Z0-9_,]*)}#', $this->regex, $matches);
preg_match(self::OPT_REGEX, $this->regex, $matches);
if ($matches) {
$repl = $this->getRegexOptionalAttributesReplacement($matches[1]);
$this->regex = str_replace($matches[0], $repl, $this->regex);
Expand All @@ -181,7 +183,7 @@ protected function setRegexOptionalAttributes()
*/
protected function getRegexOptionalAttributesReplacement($list)
{
$list = explode(',', $list);
$list = $this->getRegexOptionalAttributesReplacementList($list);
$head = $this->getRegexOptionalAttributesReplacementHead($list);
$tail = '';
foreach ($list as $name) {
Expand All @@ -192,6 +194,23 @@ protected function getRegexOptionalAttributesReplacement($list)
return $head . $tail;
}

/**
* Get list of optional attributes from regex
*
* @param $list
* @return string[]
*/
protected function getRegexOptionalAttributesReplacementList($list)
{
$list = trim($list);
$newList = preg_split(self::SPLIT_REGEX, $list);
if (false === $newList) {
return [$list];
}

return $newList;
}

/**
*
* Gets the leading portion of the optional attributes replacement.
Expand All @@ -218,19 +237,17 @@ protected function getRegexOptionalAttributesReplacementHead(&$list)
* Expands attribute names in the regex to named subpatterns; adds default
* `null` values for attributes without defaults.
*
* @return null
*
*/
protected function setRegexAttributes()
{
$find = '#{([a-z][a-zA-Z0-9_]*)}#';
$attributes = $this->route->attributes;
$newAttributes = [];
preg_match_all($find, $this->regex, $matches, PREG_SET_ORDER);
preg_match_all(self::REGEX, $this->regex, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$name = $match[1];
$subpattern = $this->getSubpattern($name);
$this->regex = str_replace("{{$name}}", $subpattern, $this->regex);
$token = isset($match[2]) ? $match[2] : null;
$subpattern = $this->getSubpattern($name, $token);
$this->regex = str_replace($match[0], $subpattern, $this->regex);
if (! isset($attributes[$name])) {
$newAttributes[$name] = null;
}
Expand All @@ -243,15 +260,21 @@ protected function setRegexAttributes()
* Returns a named subpattern for an attribute name.
*
* @param string $name The attribute name.
* @param string|null $token The pattern FastRoute format from route
*
* @return string The named subpattern.
*
*/
protected function getSubpattern($name)
protected function getSubpattern($name, $token = null)
{
// is there a custom subpattern for the name?
if (isset($this->route->tokens[$name]) && is_string($this->route->tokens[$name])) {
return "(?P<{$name}>{$this->route->tokens[$name]})";
// if $token is null use route token
$token = $token ?: $this->route->tokens[$name];
}

if ($token) {
return "(?P<{$name}>{$token})";
}

// use a default subpattern
Expand All @@ -262,8 +285,6 @@ protected function getSubpattern($name)
*
* Adds a wildcard subpattern to the end of the regex.
*
* @return null
*
*/
protected function setRegexWildcard()
{
Expand Down
Loading

0 comments on commit 52fc0be

Please sign in to comment.