diff --git a/.gitignore b/.gitignore index d85ecfa..152cb91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ composer.lock vendor/ .idea/ -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +tests/tmp \ No newline at end of file diff --git a/README.md b/README.md index 2cb77d7..f4adbaa 100644 --- a/README.md +++ b/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: diff --git a/src/Pecee/Http/Url.php b/src/Pecee/Http/Url.php index d062582..1ff5604 100644 --- a/src/Pecee/Http/Url.php +++ b/src/Pecee/Http/Url.php @@ -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; } diff --git a/src/Pecee/SimpleRouter/Route/Route.php b/src/Pecee/SimpleRouter/Route/Route.php index 401b1d7..4afcfc4 100644 --- a/src/Pecee/SimpleRouter/Route/Route.php +++ b/src/Pecee/SimpleRouter/Route/Route.php @@ -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; } diff --git a/src/Pecee/SimpleRouter/Route/RouteGroup.php b/src/Pecee/SimpleRouter/Route/RouteGroup.php index a840784..5089776 100644 --- a/src/Pecee/SimpleRouter/Route/RouteGroup.php +++ b/src/Pecee/SimpleRouter/Route/RouteGroup.php @@ -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) { diff --git a/src/Pecee/SimpleRouter/Route/RouteResource.php b/src/Pecee/SimpleRouter/Route/RouteResource.php index 587fd10..29bf4db 100644 --- a/src/Pecee/SimpleRouter/Route/RouteResource.php +++ b/src/Pecee/SimpleRouter/Route/RouteResource.php @@ -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) { diff --git a/src/Pecee/SimpleRouter/Route/RouteUrl.php b/src/Pecee/SimpleRouter/Route/RouteUrl.php index da779a4..f0e2b8a 100644 --- a/src/Pecee/SimpleRouter/Route/RouteUrl.php +++ b/src/Pecee/SimpleRouter/Route/RouteUrl.php @@ -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) { diff --git a/tests/Pecee/SimpleRouter/RouterUrlTest.php b/tests/Pecee/SimpleRouter/RouterUrlTest.php index 2581c36..ed1dff9 100644 --- a/tests/Pecee/SimpleRouter/RouterUrlTest.php +++ b/tests/Pecee/SimpleRouter/RouterUrlTest.php @@ -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; }); });