From 6fe0fff9c84586b5412072885995c40595244f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riikka=20Kalliom=C3=A4ki?= Date: Wed, 17 Apr 2019 20:30:29 +0300 Subject: [PATCH] Add support for optional middle segments --- src/RouteParser/Std.php | 69 ++++++++++++++++++++++++++---------- test/RouteParser/StdTest.php | 48 ++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/RouteParser/Std.php b/src/RouteParser/Std.php index 4fbdee1..0ac8030 100644 --- a/src/RouteParser/Std.php +++ b/src/RouteParser/Std.php @@ -24,29 +24,62 @@ class Std implements RouteParser public function parse($route) { - $routeWithoutClosingOptionals = rtrim($route, ']'); - $numOptionals = strlen($route) - strlen($routeWithoutClosingOptionals); - - // Split on [ while skipping placeholders - $segments = preg_split('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \[~x', $routeWithoutClosingOptionals); - if ($numOptionals !== count($segments) - 1) { - // If there are any ] in the middle of the route, throw a more specific error message - if (preg_match('~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | \]~x', $routeWithoutClosingOptionals)) { - throw new BadRouteException('Optional segments can only occur at the end of a route'); - } - throw new BadRouteException("Number of opening '[' and closing ']' does not match"); + if (strcspn($route, '[]') === strlen($route)) { + return [$this->parsePlaceholders($route)]; } - $currentRoute = ''; - $routeDatas = []; - foreach ($segments as $n => $segment) { - if ($segment === '' && $n !== 0) { - throw new BadRouteException('Empty optional part'); + $routeDatas = $this->consume($route); + + return array_map([$this, 'parsePlaceholders'], $routeDatas); + } + + private function consume(& $route, $recursion = false) + { + $routeDatas = ['']; + + do { + $segments = preg_split( + '~' . self::VARIABLE_REGEX . '(*SKIP)(*F) | ( \[ | ] )~x', + $route, + 2 + ); + + foreach ($routeDatas as $key => $data) { + $routeDatas[$key] = $data . $segments[0]; } - $currentRoute .= $segment; - $routeDatas[] = $this->parsePlaceholders($currentRoute); + if (isset($segments[1])) { + $delimiter = $route[strlen($segments[0])]; + $route = $segments[1]; + + if ($delimiter === ']') { + if (!$recursion) { + throw new BadRouteException("Number of opening '[' and closing ']' does not match"); + } + + if (in_array('', $routeDatas, true)) { + throw new BadRouteException('Empty optional part'); + } + + return $routeDatas; + } + + $forks = $this->consume($route, true); + + foreach ($routeDatas as $data) { + foreach ($forks as $fork) { + $routeDatas[] = $data . $fork; + } + } + } else { + $route = ''; + } + } while (isset($segments[1])); + + if ($recursion) { + throw new BadRouteException("Number of opening '[' and closing ']' does not match"); } + return $routeDatas; } diff --git a/test/RouteParser/StdTest.php b/test/RouteParser/StdTest.php index e13e4de..646c9ff 100644 --- a/test/RouteParser/StdTest.php +++ b/test/RouteParser/StdTest.php @@ -115,6 +115,46 @@ public function provideTestParse() ['/', ['_foo', '.*']] ] ], + [ + '/test[/opt]/required', + [ + ['/test/required'], + ['/test/opt/required'], + ] + ], + [ + '/test[/opt[/sub1][/sub2]]/required[/end]', + [ + ['/test/required'], + ['/test/opt/required'], + ['/test/opt/sub1/required'], + ['/test/opt/sub2/required'], + ['/test/opt/sub1/sub2/required'], + ['/test/required/end'], + ['/test/opt/required/end'], + ['/test/opt/sub1/required/end'], + ['/test/opt/sub2/required/end'], + ['/test/opt/sub1/sub2/required/end'], + ] + ], + [ + '/test[/[opt[/]]]', + [ + ['/test'], + ['/test/'], + ['/test/opt'], + ['/test/opt/'], + ] + ], + [ + '/test[/opt][/]', + [ + ['/test'], + ['/test/opt'], + ['/test/'], + ['/test/opt/'], + ] + ] ]; } @@ -133,6 +173,10 @@ public function provideTestParseError() '/testopt]', "Number of opening '[' and closing ']' does not match" ], + [ + '/test]opt', + "Number of opening '[' and closing ']' does not match" + ], [ '/test[]', 'Empty optional part' @@ -145,10 +189,6 @@ public function provideTestParseError() '[[test]]', 'Empty optional part' ], - [ - '/test[/opt]/required', - 'Optional segments can only occur at the end of a route' - ], ]; } }