From 1fb574a5bd024fc36b3eaad08cb5eeabfe2c6213 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 24 Mar 2020 20:16:13 +0000 Subject: [PATCH] NEW: Variadic URL parameter matches for url_handlers (#9438) * Add wildcard URL parameter matches for url_handlers * Extra tests for wildcard parameters * Add a PHP warning if more params appear after wildcard param --- .../02_Controllers/02_Routing.md | 45 +++++++++++++++++++ src/Control/HTTPRequest.php | 23 +++++++++- tests/php/Control/HTTPRequestTest.php | 37 +++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/docs/en/02_Developer_Guides/02_Controllers/02_Routing.md b/docs/en/02_Developer_Guides/02_Controllers/02_Routing.md index b22c3a6b4..ffa37f788 100644 --- a/docs/en/02_Developer_Guides/02_Controllers/02_Routing.md +++ b/docs/en/02_Developer_Guides/02_Controllers/02_Routing.md @@ -142,6 +142,51 @@ the `TeamController`. Match an url starting with `/admin/help/`, but don't include `/help/` as part of the action (the shift point is set to start parsing variables and the appropriate controller action AFTER the `//`). +### Wildcard URL Patterns + +As of SilverStripe 4.6 there are two wildcard patterns that can be used. `$@` and `$*`. These parameters can only be used +at the end of a URL pattern, any further rules are ignored. + +Inspired by bash variadic variable syntax there are two ways to capture all URL parameters without having to explicitly +specify them in the URL rule. + +Using `$@` will split the URL into numbered parameters (`$1`, `$2`, ..., `$n`). For example: + +```php + 'index', + ]; + + public function index($request) + { + // GET /staff/managers/bob + $request->latestParam('$1'); // managers + $request->latestParam('$2'); // bob + } +} +``` + +Alternatively, if access to the parameters is not required in this way then it is possible to use `$*` to match all +URL parameters but not collect them in the same way: + +```php + 'index', + ]; + + public function index($request) + { + // GET /staff/managers/bob + $request->remaining(); // managers/bob + } +} +``` ## URL Handlers diff --git a/src/Control/HTTPRequest.php b/src/Control/HTTPRequest.php index 46743c045..130dc8867 100644 --- a/src/Control/HTTPRequest.php +++ b/src/Control/HTTPRequest.php @@ -564,7 +564,28 @@ class HTTPRequest implements ArrayAccess /** @skipUpgrade */ $key = "Controller"; - $arguments[$varName] = isset($this->dirParts[$i]) ? $this->dirParts[$i] : null; + if ($varName === '*' || $varName === '@') { + if (isset($patternParts[$i + 1])) { + user_error(sprintf('All URL params after wildcard parameter $%s will be ignored', $varName), E_USER_WARNING); + } + if ($varName === '*') { + array_pop($patternParts); + $shiftCount = sizeof($patternParts); + $patternParts = array_merge($patternParts, array_slice($this->dirParts, $i)); + break; + } else { + array_pop($patternParts); + $shiftCount = sizeof($patternParts); + $remaining = count($this->dirParts) - $i; + for ($j = 1; $j <= $remaining; $j++) { + $arguments["$${j}"] = $this->dirParts[$j + $i - 1]; + } + $patternParts = array_merge($patternParts, array_keys($arguments)); + break; + } + } else { + $arguments[$varName] = $this->dirParts[$i] ?? null; + } if ($part == '$Controller' && ( !ClassInfo::exists($arguments[$key]) diff --git a/tests/php/Control/HTTPRequestTest.php b/tests/php/Control/HTTPRequestTest.php index 2b2cf49ad..6f06bb55e 100644 --- a/tests/php/Control/HTTPRequestTest.php +++ b/tests/php/Control/HTTPRequestTest.php @@ -25,6 +25,43 @@ class HTTPRequestTest extends SapphireTest $this->assertEquals(array("_matched" => true), $request->match('add', true)); } + /** + * @useDatabase false + */ + public function testWildCardMatch() + { + $request = new HTTPRequest('GET', 'admin/crm/test'); + $this->assertEquals(['$1' => 'crm', '$2' => 'test'], $request->match('admin/$@', true)); + $this->assertTrue($request->allParsed()); + + $request = new HTTPRequest('GET', 'admin/crm/test'); + $this->assertEquals(['_matched' => true], $request->match('admin/$*', true)); + $this->assertTrue($request->allParsed()); + $this->assertEquals('crm/test', $request->remaining()); + + $request = new HTTPRequest('GET', 'admin/crm/test/part1/part2'); + $this->assertEquals(['Action' => 'crm', '$1' => 'test', '$2' => 'part1', '$3' => 'part2'], $request->match('admin/$Action/$@', true)); + $this->assertTrue($request->allParsed()); + + $request = new HTTPRequest('GET', 'admin/crm/test/part1/part2'); + $this->assertEquals(['Action' => 'crm'], $request->match('admin/$Action/$*', true)); + $this->assertTrue($request->allParsed()); + $this->assertEquals('test/part1/part2', $request->remaining()); + } + + /** + * This test just asserts a warning is given if there is more than one wildcard parameter. Note that this isn't an + * enforcement of an API and we an add new behaviour in the future to allow many wildcard params if we want to + * + * @expectedException \PHPUnit_Framework_Error_Warning + */ + public function testWildCardWithFurtherParams() + { + $request = new HTTPRequest('GET', 'admin/crm/test'); + // all parameters after the first wildcard parameter are ignored + $request->match('admin/$Action/$@/$Other/$*', true); + } + public function testHttpMethodOverrides() { $request = new HTTPRequest(