mirror of
https://github.com/skipperbent/simple-php-router.git
synced 2026-06-15 18:23:26 +03:00
Merge pull request #638 from skipperbent/v5-feature-ending-slash
[FEATURE] Parameter ending trail/slash
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
.idea/
|
||||
.phpunit.result.cache
|
||||
.phpunit.result.cache
|
||||
tests/tmp
|
||||
33
README.md
33
README.md
@@ -36,11 +36,12 @@ You can donate any amount of your choice by [clicking here](https://www.paypal.c
|
||||
- [Available methods](#available-methods)
|
||||
- [Multiple HTTP-verbs](#multiple-http-verbs)
|
||||
- [Route parameters](#route-parameters)
|
||||
- [Required parameters](#required-parameters)
|
||||
- [Optional parameters](#optional-parameters)
|
||||
- [Regular expression constraints](#regular-expression-constraints)
|
||||
- [Regular expression route-match](#regular-expression-route-match)
|
||||
- [Custom regex for matching parameters](#custom-regex-for-matching-parameters)
|
||||
- [Required parameters](#required-parameters)
|
||||
- [Optional parameters](#optional-parameters)
|
||||
- [Including slash in parameters](#including-slash-in-parameters)
|
||||
- [Regular expression constraints](#regular-expression-constraints)
|
||||
- [Regular expression route-match](#regular-expression-route-match)
|
||||
- [Custom regex for matching parameters](#custom-regex-for-matching-parameters)
|
||||
- [Named routes](#named-routes)
|
||||
- [Generating URLs To Named Routes](#generating-urls-to-named-routes)
|
||||
- [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
|
||||
|
||||
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:
|
||||
|
||||
@@ -42,6 +42,12 @@ class Url implements JsonSerializable
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* Original path with no sanitization to ending slash
|
||||
* @var string|null
|
||||
*/
|
||||
private $originalPath;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@@ -73,6 +79,7 @@ class Url implements JsonSerializable
|
||||
|
||||
if (isset($data['path']) === true) {
|
||||
$this->setPath($data['path']);
|
||||
$this->originalPath = $data['path'];
|
||||
}
|
||||
|
||||
$this->fragment = $data['fragment'] ?? null;
|
||||
@@ -226,6 +233,15 @@ class Url implements JsonSerializable
|
||||
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
|
||||
*
|
||||
@@ -284,7 +300,7 @@ class Url implements JsonSerializable
|
||||
$params = [];
|
||||
parse_str($queryString, $params);
|
||||
|
||||
if(count($params) > 0) {
|
||||
if (count($params) > 0) {
|
||||
return $this->setParams($params);
|
||||
}
|
||||
|
||||
@@ -469,7 +485,7 @@ class Url implements JsonSerializable
|
||||
{
|
||||
$path = $this->path ?? '/';
|
||||
|
||||
if($includeParams === false) {
|
||||
if ($includeParams === false) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ abstract class Route implements IRoute
|
||||
*/
|
||||
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.
|
||||
* @var string|null
|
||||
@@ -111,7 +117,7 @@ abstract class Route implements IRoute
|
||||
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 :
|
||||
sprintf
|
||||
@@ -123,8 +129,10 @@ abstract class Route implements IRoute
|
||||
);
|
||||
|
||||
// Ensures that host names/domains will work with parameters
|
||||
|
||||
if($route[0] == '{') $url = '/' . ltrim($url, '/');
|
||||
if ($route[0] === $this->paramModifiers[0]) {
|
||||
$url = '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
$urlRegex = '';
|
||||
$parameters = [];
|
||||
|
||||
@@ -132,7 +140,7 @@ abstract class Route implements IRoute
|
||||
$urlRegex = preg_quote($route, '/');
|
||||
} else {
|
||||
|
||||
foreach (preg_split('/((\.?-?\/?){[^}]+})/', $route) as $key => $t) {
|
||||
foreach (preg_split('/((\.?-?\/?){[^' . $this->paramModifiers[1] . ']+' . $this->paramModifiers[1] . ')/', $route) as $key => $t) {
|
||||
|
||||
$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) {
|
||||
return null;
|
||||
}
|
||||
@@ -167,7 +176,8 @@ abstract class Route implements IRoute
|
||||
$lastParams = [];
|
||||
|
||||
/* 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
|
||||
if (isset($groupParameters[$name]) === true) {
|
||||
@@ -175,10 +185,16 @@ abstract class Route implements IRoute
|
||||
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 = array_merge($values, $lastParams);
|
||||
$values += $lastParams;
|
||||
}
|
||||
|
||||
$this->originalParameters = $values;
|
||||
@@ -387,6 +403,17 @@ abstract class Route implements IRoute
|
||||
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.
|
||||
*
|
||||
@@ -416,6 +443,10 @@ abstract class Route implements IRoute
|
||||
$values['defaultParameterRegex'] = $this->defaultParameterRegex;
|
||||
}
|
||||
|
||||
if ($this->slashParameterEnabled === true) {
|
||||
$values['includeSlash'] = $this->slashParameterEnabled;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
@@ -453,6 +484,10 @@ abstract class Route implements IRoute
|
||||
$this->setDefaultParameterRegex($settings['defaultParameterRegex']);
|
||||
}
|
||||
|
||||
if (isset($settings['includeSlash']) === true) {
|
||||
$this->setSlashParameterEnabled($settings['includeSlash']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class RouteGroup extends Route implements IGroupRoute
|
||||
return true;
|
||||
}
|
||||
|
||||
$parameters = $this->parseParameters($domain, $request->getHost(), '.*');
|
||||
$parameters = $this->parseParameters($domain, $request->getHost(), $request, '.*');
|
||||
|
||||
if ($parameters !== null && count($parameters) !== 0) {
|
||||
$this->parameters = $parameters;
|
||||
@@ -60,7 +60,7 @@ class RouteGroup extends Route implements IGroupRoute
|
||||
|
||||
if ($this->prefix !== null) {
|
||||
/* 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 ($parameters === null) {
|
||||
|
||||
@@ -99,7 +99,7 @@ class RouteResource extends LoadableRoute implements IControllerRoute
|
||||
$route = rtrim($this->url, '/') . '/{id?}/{action?}';
|
||||
|
||||
/* 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 ($regexMatch === null && $this->parameters === null) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class RouteUrl extends LoadableRoute
|
||||
}
|
||||
|
||||
/* 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 ($regexMatch === null && $parameters === null) {
|
||||
|
||||
@@ -27,6 +27,23 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
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()
|
||||
{
|
||||
// Test spanish characters
|
||||
@@ -191,7 +208,7 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$results = '';
|
||||
|
||||
TestRouter::get('/tester/{param}', function ($param = null) use($results) {
|
||||
TestRouter::get('/tester/{param}', function ($param = null) use ($results) {
|
||||
return $results = $param;
|
||||
})->setMatch('/(.*)/i');
|
||||
|
||||
@@ -234,9 +251,9 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
TestRouter::debug('/');
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertCount(2, $result);
|
||||
}
|
||||
|
||||
|
||||
public function testDefaultNamespace()
|
||||
{
|
||||
TestRouter::setDefaultNamespace('\\Default\\Namespace');
|
||||
@@ -245,14 +262,14 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
TestRouter::group([
|
||||
'namespace' => 'Appended\Namespace',
|
||||
'prefix' => '/horses',
|
||||
'prefix' => '/horses',
|
||||
], function () {
|
||||
|
||||
TestRouter::get('/', 'DummyController@method1');
|
||||
|
||||
TestRouter::group([
|
||||
'namespace' => '\\New\\Namespace',
|
||||
'prefix' => '/race',
|
||||
'prefix' => '/race',
|
||||
], function () {
|
||||
|
||||
TestRouter::get('/', 'DummyController@method1');
|
||||
@@ -287,13 +304,14 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
TestRouter::router()->reset();
|
||||
}
|
||||
|
||||
public function testGroupPrefix() {
|
||||
public function testGroupPrefix()
|
||||
{
|
||||
|
||||
$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;
|
||||
});
|
||||
});
|
||||
@@ -307,13 +325,13 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
$result = null;
|
||||
$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;
|
||||
});
|
||||
|
||||
TestRouter::get('/user/{userId}', function($userId) use(&$result) {
|
||||
TestRouter::get('/user/{userId}', function ($userId) use (&$result) {
|
||||
$result = $userId;
|
||||
});
|
||||
});
|
||||
@@ -324,14 +342,15 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
}
|
||||
|
||||
public function testPassParameter() {
|
||||
public function testPassParameter()
|
||||
{
|
||||
|
||||
$result = false;
|
||||
$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;
|
||||
});
|
||||
|
||||
@@ -343,15 +362,16 @@ class RouterUrlTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
}
|
||||
|
||||
public function testPassParameterDeep() {
|
||||
public function testPassParameterDeep()
|
||||
{
|
||||
|
||||
$result = false;
|
||||
$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::get('/test', function($language) use(&$result) {
|
||||
TestRouter::group(['prefix' => '/admin'], function ($language) use (&$result) {
|
||||
TestRouter::get('/test', function ($language) use (&$result) {
|
||||
$result = $language;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user