Compare commits

...

17 Commits

Author SHA1 Message Date
Simon Sessingø 5dbfc3dbfe Merge pull request #639 from skipperbent/v5-development
Version 5.1.0.0
2023-03-25 03:19:13 +01:00
Simon Sessingø 8be42ca1a6 Merge pull request #638 from skipperbent/v5-feature-ending-slash
[FEATURE] Parameter ending trail/slash
2023-03-25 03:13:28 +01:00
sessingo 4a7360909c Fixed for PHP7 2023-03-25 02:24:19 +01:00
sessingo 74c52931e9 Ending trail/slash feature
- Feature: added support for slash in parameters (see readme).
- Route: Fixed hardcoded param modifier.
- Route: optimisations.
- Updated Readme.
2023-03-25 02:20:06 +01:00
Simon Sessingø 0aea8673d9 Merge pull request #632 from skipperbent/v5-development
Version 5.0.0.3
2023-02-13 14:06:24 +01:00
sessingo 5ab5087f8e Fixed CSRF-token not triggering exception handlers. 2023-02-13 13:59:49 +01:00
sessingo b7c1b52a57 Updated phpstan 2023-02-12 23:39:49 +01:00
Simon Sessingø 9c66a4dfd8 Merge pull request #630 from skipperbent/v5-development
Version 5.0.0.2
2023-02-11 17:34:48 +01:00
sessingo 941149d8d7 Fixed deprication warnings 2023-02-11 17:31:00 +01:00
sessingo 1764b67112 Updated composer.json 2023-02-11 17:25:54 +01:00
sessingo 3742998537 Updated git workflows 2023-02-11 17:22:40 +01:00
sessingo 20e00efbed Php8 deprication warning 2023-02-11 17:16:40 +01:00
sessingo 7dd176a771 Fixed strtolower php8 deprication warning 2023-02-10 22:47:45 +01:00
sessingo abda9d468b Fixed deprication message when using response()->json 2023-02-10 08:48:58 +01:00
sessingo 23a29ce5d1 Fixed php strtolower deprication warning 2023-02-10 06:10:29 +01:00
Simon Sessingø d5bf77cbd4 Merge pull request #628 from skipperbent/v5-development
Fixed offsetGet return type deprication warning
2023-02-09 03:34:48 +01:00
sessingo e34fe47a04 Fixed offsetGet return type deprication warning 2023-02-09 03:29:41 +01:00
16 changed files with 160 additions and 56 deletions
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
- ubuntu-latest
- windows-latest
php-version:
- 7.1
- 7.4
- 8.0
phpunit-version:
- 7.5.20
- 8.5.32
dependencies:
- lowest
- highest
+2 -1
View File
@@ -1,4 +1,5 @@
composer.lock
vendor/
.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)
- [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:
+10 -5
View File
@@ -31,12 +31,12 @@
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^7",
"phpunit/phpunit": "^8",
"mockery/mockery": "^1",
"phpstan/phpstan": "^0",
"phpstan/phpstan-phpunit": "^0",
"phpstan/phpstan-deprecation-rules": "^0",
"phpstan/phpstan-strict-rules": "^0"
"phpstan/phpstan": "^1",
"phpstan/phpstan-phpunit": "^1",
"phpstan/phpstan-deprecation-rules": "^1",
"phpstan/phpstan-strict-rules": "^1"
},
"scripts": {
"test": [
@@ -47,5 +47,10 @@
"psr-4": {
"Pecee\\": "src/Pecee/"
}
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true
}
}
}
+1 -1
View File
@@ -81,7 +81,7 @@ class InputItem implements ArrayAccess, IInputItem, IteratorAggregate
return isset($this->value[$offset]);
}
public function offsetGet($offset)
public function offsetGet($offset): ?self
{
if ($this->offsetExists($offset) === true) {
return $this->value[$offset];
+2 -2
View File
@@ -133,7 +133,7 @@ class Request
if($url !== null){
$this->setUrl(new Url($url));
}else{
$this->setUrl(new Url(urldecode($this->getHeader('request-uri'))));
$this->setUrl(new Url(urldecode((string)$this->getHeader('request-uri'))));
}
$this->setContentType((string)$this->getHeader('content-type'));
$this->setMethod((string)($_POST[static::FORCE_METHOD_KEY] ?? $this->getHeader('request-method')));
@@ -364,7 +364,7 @@ class Request
*/
public function isAjax(): bool
{
return (strtolower($this->getHeader('http-x-requested-with')) === 'xmlhttprequest');
return (strtolower((string)$this->getHeader('http-x-requested-with')) === 'xmlhttprequest');
}
/**
+2 -2
View File
@@ -87,11 +87,11 @@ class Response
/**
* Json encode
* @param array|JsonSerializable $value
* @param ?int $options JSON options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT, JSON_PRESERVE_ZERO_FRACTION, JSON_UNESCAPED_UNICODE, JSON_PARTIAL_OUTPUT_ON_ERROR.
* @param int $options JSON options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT, JSON_PRESERVE_ZERO_FRACTION, JSON_UNESCAPED_UNICODE, JSON_PARTIAL_OUTPUT_ON_ERROR.
* @param int $dept JSON debt.
* @throws InvalidArgumentException
*/
public function json($value, ?int $options = null, int $dept = 512): void
public function json($value, int $options = 0, int $dept = 512): void
{
if (($value instanceof JsonSerializable) === false && is_array($value) === false) {
throw new InvalidArgumentException('Invalid type for parameter "value". Must be of type array or object implementing the \JsonSerializable interface.');
+18 -2
View File
@@ -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;
}
@@ -167,7 +167,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute
if (stripos($url, $param1) !== false || stripos($url, $param) !== false) {
/* Add parameter to the correct position */
$url = str_ireplace([sprintf($param1, $param), sprintf($param2, $param)], $value, $url);
$url = str_ireplace([sprintf($param1, $param), sprintf($param2, $param)], (string)$value, $url);
} else {
/* Parameter aren't recognized and will be appended at the end of the url */
$url .= $value . '/';
@@ -195,7 +195,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute
*/
public function hasName(string $name): bool
{
return strtolower($this->name) === strtolower($name);
return strtolower((string)$this->name) === strtolower((string)$name);
}
/**
+41 -6
View File
@@ -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;
}
@@ -81,7 +81,7 @@ class RouteController extends LoadableRoute implements IControllerRoute
$url .= '//' . $group->getDomains()[0];
}
$url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower($method) . implode('/', $parameters);
$url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower((string)$method) . implode('/', $parameters);
return '/' . trim($url, '/') . '/';
}
+3 -3
View File
@@ -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) {
@@ -74,7 +74,7 @@ class RouteGroup extends Route implements IGroupRoute
$parsedPrefix = $this->prefix;
foreach ($this->getParameters() as $parameter => $value) {
$parsedPrefix = str_ireplace('{' . $parameter . '}', $value, $parsedPrefix);
$parsedPrefix = str_ireplace('{' . $parameter . '}', (string)$value, (string)$parsedPrefix);
}
/* Skip if prefix doesn't match */
@@ -99,14 +99,14 @@ 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) {
return false;
}
$action = strtolower(trim($this->parameters['action']));
$action = strtolower(trim((string)$this->parameters['action']));
$id = $this->parameters['id'];
// Remove action parameter
+1 -1
View File
@@ -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) {
+7 -3
View File
@@ -342,8 +342,12 @@ class Router
'csrfVerifier' => $this->csrfVerifier,
]);
/* Verify csrf token for request */
$this->csrfVerifier->handle($this->request);
try {
/* Verify csrf token for request */
$this->csrfVerifier->handle($this->request);
} catch(\Exception $e) {
$this->handleException($e);
}
}
$output = $this->routeRequest();
@@ -608,7 +612,7 @@ class Router
if (strpos($name, '@') !== false) {
[$controller, $method] = array_map('strtolower', explode('@', $name));
if ($controller === strtolower($route->getClass()) && $method === strtolower($route->getMethod())) {
if ($controller === strtolower((string)$route->getClass()) && $method === strtolower((string)$route->getMethod())) {
$this->debug('Found route "%s" by controller "%s" and method "%s"', $route->getUrl(), $controller, $method);
return $route;
+38 -18
View File
@@ -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;
});
});