mirror of
https://github.com/skipperbent/simple-php-router.git
synced 2026-06-17 08:47:52 +00:00
Merge pull request #638 from skipperbent/v5-feature-ending-slash
[FEATURE] Parameter ending trail/slash
This commit is contained in:
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
composer.lock
|
composer.lock
|
||||||
vendor/
|
vendor/
|
||||||
.idea/
|
.idea/
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
tests/tmp
|
||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user