Merge pull request #638 from skipperbent/v5-feature-ending-slash

[FEATURE] Parameter ending trail/slash
This commit is contained in:
Simon Sessingø
2023-03-25 03:13:28 +01:00
committed by GitHub
8 changed files with 131 additions and 36 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
composer.lock composer.lock
vendor/ vendor/
.idea/ .idea/
.phpunit.result.cache .phpunit.result.cache
tests/tmp
+28 -5
View File
@@ -36,11 +36,12 @@ You can donate any amount of your choice by [clicking here](https://www.paypal.c
- [Available methods](#available-methods) - [Available methods](#available-methods)
- [Multiple HTTP-verbs](#multiple-http-verbs) - [Multiple HTTP-verbs](#multiple-http-verbs)
- [Route parameters](#route-parameters) - [Route parameters](#route-parameters)
- [Required parameters](#required-parameters) - [Required parameters](#required-parameters)
- [Optional parameters](#optional-parameters) - [Optional parameters](#optional-parameters)
- [Regular expression constraints](#regular-expression-constraints) - [Including slash in parameters](#including-slash-in-parameters)
- [Regular expression route-match](#regular-expression-route-match) - [Regular expression constraints](#regular-expression-constraints)
- [Custom regex for matching parameters](#custom-regex-for-matching-parameters) - [Regular expression route-match](#regular-expression-route-match)
- [Custom regex for matching parameters](#custom-regex-for-matching-parameters)
- [Named routes](#named-routes) - [Named routes](#named-routes)
- [Generating URLs To Named Routes](#generating-urls-to-named-routes) - [Generating URLs To Named Routes](#generating-urls-to-named-routes)
- [Router groups](#router-groups) - [Router groups](#router-groups)
@@ -490,6 +491,28 @@ SimpleRouter::get('/user/{name?}', function ($name = 'Simon') {
}); });
``` ```
### Including slash in parameters
If you're working with WebDAV services the url could mean the difference between a file and a folder.
For instance `/path` will be considered a file - whereas `/path/` will be considered a folder.
The router can add the ending slash for the last parameter in your route based on the path. So if `/path/` is requested the parameter will contain the value of `path/` and visa versa.
To ensure compatibility with older versions, this feature is disabled by default and has to be enabled by setting
the `setSettings(['includeSlash' => true])` or by using setting `setSlashParameterEnabled(true)` for your route.
**Example**
```php
SimpleRouter::get('/path/{fileOrFolder}', function ($fileOrFolder) {
return $fileOrFolder;
})->setSettings(['includeSlash' => true]);
```
- Requesting `/path/file` will return the `$fileOrFolder` value: `file`.
- Requesting `/path/folder/` will return the `$fileOrFolder` value: `folder/`.
### Regular expression constraints ### Regular expression constraints
You may constrain the format of your route parameters using the where method on a route instance. The where method accepts the name of the parameter and a regular expression defining how the parameter should be constrained: You may constrain the format of your route parameters using the where method on a route instance. The where method accepts the name of the parameter and a regular expression defining how the parameter should be constrained:
+18 -2
View File
@@ -42,6 +42,12 @@ class Url implements JsonSerializable
*/ */
private $path; private $path;
/**
* Original path with no sanitization to ending slash
* @var string|null
*/
private $originalPath;
/** /**
* @var array * @var array
*/ */
@@ -73,6 +79,7 @@ class Url implements JsonSerializable
if (isset($data['path']) === true) { if (isset($data['path']) === true) {
$this->setPath($data['path']); $this->setPath($data['path']);
$this->originalPath = $data['path'];
} }
$this->fragment = $data['fragment'] ?? null; $this->fragment = $data['fragment'] ?? null;
@@ -226,6 +233,15 @@ class Url implements JsonSerializable
return $this->path ?? '/'; return $this->path ?? '/';
} }
/**
* Get original path with no sanitization of ending trail/slash.
* @return string|null
*/
public function getOriginalPath(): ?string
{
return $this->originalPath;
}
/** /**
* Set the url path * Set the url path
* *
@@ -284,7 +300,7 @@ class Url implements JsonSerializable
$params = []; $params = [];
parse_str($queryString, $params); parse_str($queryString, $params);
if(count($params) > 0) { if (count($params) > 0) {
return $this->setParams($params); return $this->setParams($params);
} }
@@ -469,7 +485,7 @@ class Url implements JsonSerializable
{ {
$path = $this->path ?? '/'; $path = $this->path ?? '/';
if($includeParams === false) { if ($includeParams === false) {
return $path; return $path;
} }
+41 -6
View File
@@ -20,6 +20,12 @@ abstract class Route implements IRoute
*/ */
protected $filterEmptyParams = true; protected $filterEmptyParams = true;
/**
* If true the last parameter of the route will include ending trail/slash.
* @var bool
*/
protected $slashParameterEnabled = false;
/** /**
* Default regular expression used for parsing parameters. * Default regular expression used for parsing parameters.
* @var string|null * @var string|null
@@ -111,7 +117,7 @@ abstract class Route implements IRoute
return $router->getClassLoader()->loadClassMethod($class, $method, $parameters); return $router->getClassLoader()->loadClassMethod($class, $method, $parameters);
} }
protected function parseParameters($route, $url, $parameterRegex = null): ?array protected function parseParameters($route, $url, Request $request, $parameterRegex = null): ?array
{ {
$regex = (strpos($route, $this->paramModifiers[0]) === false) ? null : $regex = (strpos($route, $this->paramModifiers[0]) === false) ? null :
sprintf sprintf
@@ -123,8 +129,10 @@ abstract class Route implements IRoute
); );
// Ensures that host names/domains will work with parameters // Ensures that host names/domains will work with parameters
if ($route[0] === $this->paramModifiers[0]) {
if($route[0] == '{') $url = '/' . ltrim($url, '/'); $url = '/' . ltrim($url, '/');
}
$urlRegex = ''; $urlRegex = '';
$parameters = []; $parameters = [];
@@ -132,7 +140,7 @@ abstract class Route implements IRoute
$urlRegex = preg_quote($route, '/'); $urlRegex = preg_quote($route, '/');
} else { } else {
foreach (preg_split('/((\.?-?\/?){[^}]+})/', $route) as $key => $t) { foreach (preg_split('/((\.?-?\/?){[^' . $this->paramModifiers[1] . ']+' . $this->paramModifiers[1] . ')/', $route) as $key => $t) {
$regex = ''; $regex = '';
@@ -154,6 +162,7 @@ abstract class Route implements IRoute
} }
} }
// Get name of last param
if (trim($urlRegex) === '' || (bool)preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === false) { if (trim($urlRegex) === '' || (bool)preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === false) {
return null; return null;
} }
@@ -167,7 +176,8 @@ abstract class Route implements IRoute
$lastParams = []; $lastParams = [];
/* Only take matched parameters with name */ /* Only take matched parameters with name */
foreach ((array)$parameters[1] as $name) { $originalPath = $request->getUrl()->getOriginalPath();
foreach ((array)$parameters[1] as $i => $name) {
// Ignore parent parameters // Ignore parent parameters
if (isset($groupParameters[$name]) === true) { if (isset($groupParameters[$name]) === true) {
@@ -175,10 +185,16 @@ abstract class Route implements IRoute
continue; continue;
} }
// If last parameter and slash parameter is enabled, use slash according to original path (non sanitized version)
$lastParameter = $this->paramModifiers[0] . $name . $this->paramModifiers[1] . '/';
if ($this->slashParameterEnabled && ($i === count($parameters[1]) - 1) && (substr_compare($route, $lastParameter, -strlen($lastParameter)) === 0) && $originalPath[strlen($originalPath) - 1] === '/') {
$matches[$name] .= '/';
}
$values[$name] = (isset($matches[$name]) === true && $matches[$name] !== '') ? $matches[$name] : null; $values[$name] = (isset($matches[$name]) === true && $matches[$name] !== '') ? $matches[$name] : null;
} }
$values = array_merge($values, $lastParams); $values += $lastParams;
} }
$this->originalParameters = $values; $this->originalParameters = $values;
@@ -387,6 +403,17 @@ abstract class Route implements IRoute
return $this->namespace ?? $this->defaultNamespace; return $this->namespace ?? $this->defaultNamespace;
} }
public function setSlashParameterEnabled(bool $enabled): self
{
$this->slashParameterEnabled = $enabled;
return $this;
}
public function getSlashParameterEnabled(): bool
{
return $this->slashParameterEnabled;
}
/** /**
* Export route settings to array so they can be merged with another route. * Export route settings to array so they can be merged with another route.
* *
@@ -416,6 +443,10 @@ abstract class Route implements IRoute
$values['defaultParameterRegex'] = $this->defaultParameterRegex; $values['defaultParameterRegex'] = $this->defaultParameterRegex;
} }
if ($this->slashParameterEnabled === true) {
$values['includeSlash'] = $this->slashParameterEnabled;
}
return $values; return $values;
} }
@@ -453,6 +484,10 @@ abstract class Route implements IRoute
$this->setDefaultParameterRegex($settings['defaultParameterRegex']); $this->setDefaultParameterRegex($settings['defaultParameterRegex']);
} }
if (isset($settings['includeSlash']) === true) {
$this->setSlashParameterEnabled($settings['includeSlash']);
}
return $this; return $this;
} }
+2 -2
View File
@@ -33,7 +33,7 @@ class RouteGroup extends Route implements IGroupRoute
return true; return true;
} }
$parameters = $this->parseParameters($domain, $request->getHost(), '.*'); $parameters = $this->parseParameters($domain, $request->getHost(), $request, '.*');
if ($parameters !== null && count($parameters) !== 0) { if ($parameters !== null && count($parameters) !== 0) {
$this->parameters = $parameters; $this->parameters = $parameters;
@@ -60,7 +60,7 @@ class RouteGroup extends Route implements IGroupRoute
if ($this->prefix !== null) { if ($this->prefix !== null) {
/* Parse parameters from current route */ /* Parse parameters from current route */
$parameters = $this->parseParameters($this->prefix, $url); $parameters = $this->parseParameters($this->prefix, $url, $request);
/* If no custom regular expression or parameters was found on this route, we stop */ /* If no custom regular expression or parameters was found on this route, we stop */
if ($parameters === null) { if ($parameters === null) {
@@ -99,7 +99,7 @@ class RouteResource extends LoadableRoute implements IControllerRoute
$route = rtrim($this->url, '/') . '/{id?}/{action?}'; $route = rtrim($this->url, '/') . '/{id?}/{action?}';
/* Parse parameters from current route */ /* Parse parameters from current route */
$this->parameters = $this->parseParameters($route, $url); $this->parameters = $this->parseParameters($route, $url, $request);
/* If no custom regular expression or parameters was found on this route, we stop */ /* If no custom regular expression or parameters was found on this route, we stop */
if ($regexMatch === null && $this->parameters === null) { if ($regexMatch === null && $this->parameters === null) {
+1 -1
View File
@@ -31,7 +31,7 @@ class RouteUrl extends LoadableRoute
} }
/* Parse parameters from current route */ /* Parse parameters from current route */
$parameters = $this->parseParameters($this->url, $url); $parameters = $this->parseParameters($this->url, $url, $request);
/* If no custom regular expression or parameters was found on this route, we stop */ /* If no custom regular expression or parameters was found on this route, we stop */
if ($regexMatch === null && $parameters === null) { if ($regexMatch === null && $parameters === null) {
+38 -18
View File
@@ -27,6 +27,23 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
TestRouter::router()->reset(); TestRouter::router()->reset();
} }
public function testLastParameterSlash()
{
TestRouter::get('/test/{param}', function ($param) {
return $param;
})->setSettings(['includeSlash' => true]);
// Test with ending /
$output = TestRouter::debugOutputNoReset('/test/param/');
$this->assertEquals($output, 'param/');
// Test without ending /
$output = TestRouter::debugOutputNoReset('/test/param');
$this->assertEquals($output, 'param');
TestRouter::router()->reset();
}
public function testUnicodeCharacters() public function testUnicodeCharacters()
{ {
// Test spanish characters // Test spanish characters
@@ -191,7 +208,7 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
$results = ''; $results = '';
TestRouter::get('/tester/{param}', function ($param = null) use($results) { TestRouter::get('/tester/{param}', function ($param = null) use ($results) {
return $results = $param; return $results = $param;
})->setMatch('/(.*)/i'); })->setMatch('/(.*)/i');
@@ -234,9 +251,9 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
TestRouter::debug('/'); TestRouter::debug('/');
$this->assertCount(2, $result); $this->assertCount(2, $result);
} }
public function testDefaultNamespace() public function testDefaultNamespace()
{ {
TestRouter::setDefaultNamespace('\\Default\\Namespace'); TestRouter::setDefaultNamespace('\\Default\\Namespace');
@@ -245,14 +262,14 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
TestRouter::group([ TestRouter::group([
'namespace' => 'Appended\Namespace', 'namespace' => 'Appended\Namespace',
'prefix' => '/horses', 'prefix' => '/horses',
], function () { ], function () {
TestRouter::get('/', 'DummyController@method1'); TestRouter::get('/', 'DummyController@method1');
TestRouter::group([ TestRouter::group([
'namespace' => '\\New\\Namespace', 'namespace' => '\\New\\Namespace',
'prefix' => '/race', 'prefix' => '/race',
], function () { ], function () {
TestRouter::get('/', 'DummyController@method1'); TestRouter::get('/', 'DummyController@method1');
@@ -287,13 +304,14 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
TestRouter::router()->reset(); TestRouter::router()->reset();
} }
public function testGroupPrefix() { public function testGroupPrefix()
{
$result = false; $result = false;
TestRouter::group(['prefix' => '/lang/{lang}'], function () use(&$result) { TestRouter::group(['prefix' => '/lang/{lang}'], function () use (&$result) {
TestRouter::get('/test', function() use(&$result) { TestRouter::get('/test', function () use (&$result) {
$result = true; $result = true;
}); });
}); });
@@ -307,13 +325,13 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
$result = null; $result = null;
$expectedResult = 28; $expectedResult = 28;
TestRouter::group(['prefix' => '/lang/{lang}'], function () use(&$result) { TestRouter::group(['prefix' => '/lang/{lang}'], function () use (&$result) {
TestRouter::get('/horse/{horseType}', function($horseType) use(&$result) { TestRouter::get('/horse/{horseType}', function ($horseType) use (&$result) {
$result = false; $result = false;
}); });
TestRouter::get('/user/{userId}', function($userId) use(&$result) { TestRouter::get('/user/{userId}', function ($userId) use (&$result) {
$result = $userId; $result = $userId;
}); });
}); });
@@ -324,14 +342,15 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
} }
public function testPassParameter() { public function testPassParameter()
{
$result = false; $result = false;
$expectedLanguage = 'da'; $expectedLanguage = 'da';
TestRouter::group(['prefix' => '/lang/{lang}'], function ($language) use(&$result) { TestRouter::group(['prefix' => '/lang/{lang}'], function ($language) use (&$result) {
TestRouter::get('/test', function($language) use(&$result) { TestRouter::get('/test', function ($language) use (&$result) {
$result = $language; $result = $language;
}); });
@@ -343,15 +362,16 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
} }
public function testPassParameterDeep() { public function testPassParameterDeep()
{
$result = false; $result = false;
$expectedLanguage = 'da'; $expectedLanguage = 'da';
TestRouter::group(['prefix' => '/lang/{lang}'], function ($language) use(&$result) { TestRouter::group(['prefix' => '/lang/{lang}'], function ($language) use (&$result) {
TestRouter::group(['prefix' => '/admin'], function($language) use(&$result) { TestRouter::group(['prefix' => '/admin'], function ($language) use (&$result) {
TestRouter::get('/test', function($language) use(&$result) { TestRouter::get('/test', function ($language) use (&$result) {
$result = $language; $result = $language;
}); });
}); });