From 375851d2c2f567cf26f15efffea7096de9065553 Mon Sep 17 00:00:00 2001 From: JoostK Date: Mon, 4 Feb 2013 02:31:26 +0100 Subject: [PATCH] Allow for segmented parameters as proposed in #176 --- src/Illuminate/Routing/Route.php | 58 ++++++++++++++++++++++++++++ src/Illuminate/Routing/Router.php | 55 +++++++++++++++++++++++++- tests/Routing/RoutingTest.php | 64 +++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Routing/Route.php b/src/Illuminate/Routing/Route.php index d5288ed1ffcf..aca7827a873c 100644 --- a/src/Illuminate/Routing/Route.php +++ b/src/Illuminate/Routing/Route.php @@ -27,6 +27,13 @@ class Route extends BaseRoute { */ protected $parsedParameters; + /** + * The registered segmented keys. + * + * @var array + */ + protected $segmented = array(); + /** * Execute the route and return the response. * @@ -193,6 +200,10 @@ public function getParameters() $variables = $this->compile()->getVariables(); + // We will now reduce the amount of variables to the amount of input parameters + // so that segmented parameters will be joined together into one array. + $variables = $this->reduceSegmentedVariables($variables); + // To get the parameter array, we need to spin the names of the variables on // the compiled route and match them to the parameters that we got when a // route is matched by the router, as routes instances don't have them. @@ -206,6 +217,42 @@ public function getParameters() return $this->parsedParameters = $parameters; } + /** + * Reduce all segmented variables back to one input parameter. + * + * @param array $variables + * @return array + */ + protected function reduceSegmentedVariables($variables) + { + foreach ($this->segmented as $variable => $segments) + { + // To replace the segmented variables with their original key, we need + // the position of the first variable so we can replace all variables + // from that position with just the one variable. + $offset = array_search(head($segments), $variables); + + array_splice($variables, $offset, count($segments), $variable); + + // It is also necessary to actually provide the segmented parameter so we + // will create it by combining all segments for the substitude parameter. + $this->combineParameters($segments, $variable); + } + + return $variables; + } + + /** + * Create the input parameter for reduced segments. + * + * @param array $keys + * @return void + */ + protected function combineParameters($keys, $substitute) + { + $this->parameters[$substitute] = array_only($this->parameters, $keys); + } + /** * Resolve a parameter value for the route. * @@ -398,6 +445,17 @@ public function setParameters($parameters) $this->parameters = $parameters; } + /** + * Set the segmented parameters on the route. + * + * @param array $segmented + * @return void + */ + public function setSegmented($segmented) + { + $this->segmented = $segmented; + } + /** * Set the Router instance on the route. * diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index 106fe10213ed..29600626b398 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -73,6 +73,13 @@ class Router { */ protected $binders = array(); + /** + * The registered segmented keys. + * + * @var array + */ + protected $segmented = array(); + /** * The current request being dispatched. * @@ -522,7 +529,11 @@ protected function createRoute($method, $pattern, $action) $action = array_merge($this->groupStack[$index], $action); } - // Next we will parse the pattern and add any specified prefix to the it so + // We will first replace all parameters that represent multiple segments by + // expanding them to get the fully qualified pattern containing all parameters. + list($pattern, $segmented) = $this->parseSegmentedVariables($pattern); + + // Next we will parse the pattern and add any specified prefix to it so // a common URI prefix may be specified for a group of routes easily and // without having to specify them all for every route that is defined. list($pattern, $optional) = $this->getOptional($pattern); @@ -545,6 +556,8 @@ protected function createRoute($method, $pattern, $action) $route->setRequirement('_method', $method); + $route->setSegmented($segmented); + // Once we have created the route, we will add them to our route collection // which contains all the other routes and is used to match on incoming // URL and their appropriate route destination and on URL generation. @@ -638,6 +651,34 @@ protected function setAttributes(Route $route, $action, $optional) } } + /** + * Parse the pattern by replacing al defined segmented parameters with + * their substituted named segments. + * + * @param string $pattern + * @return array + */ + protected function parseSegmentedVariables($pattern) + { + $segmented = array(); + + foreach ($this->segmented as $variable => $segments) + { + $search = '{'.$variable.'}'; + + if (str_contains($pattern, $search)) + { + $replace = '{'.implode('}/{', $segments).'}'; + + $pattern = str_replace($search, $replace, $pattern); + + $segmented[$variable] = $segments; + } + } + + return array($pattern, $segmented); + } + /** * Modify the pattern and extract optional parameters. * @@ -1083,6 +1124,18 @@ public function performBinding($key, $value, $route) return call_user_func($this->binders[$key], $value, $route); } + /** + * Register a key which will be substituted as multiple segments. + * + * @param string $key + * @param array $segments + * @return void + */ + public function segments($key, array $segments) + { + $this->segmented[$key] = $segments; + } + /** * Prepare the given value as a Response object. * diff --git a/tests/Routing/RoutingTest.php b/tests/Routing/RoutingTest.php index 38dd7d326690..60d410ec63f6 100644 --- a/tests/Routing/RoutingTest.php +++ b/tests/Routing/RoutingTest.php @@ -495,6 +495,70 @@ public function testRoutesArentOverriddenBySubDomainWithGroups() $this->assertEquals('sub', $router->dispatch($request)->getContent()); } + public function testSegmentedParametersAreSubstituted() + { + $router = new Router(new Illuminate\Container\Container); + $router->segments('user', array('firstname', 'lastname')); + $router->get('users/{user}', function($user) { return $user['firstname'].$user['lastname']; } ); + $router->get('users/{user}/{age}', function($user, $age) { return $user['firstname'].$user['lastname'].$age; } ); + + $request = Request::create('/users/taylor/otwell', 'GET'); + $this->assertEquals('taylorotwell', $router->dispatch($request)->getContent()); + + $request = Request::create('/users/taylor/otwell/25', 'GET'); + $this->assertEquals('taylorotwell25', $router->dispatch($request)->getContent()); + } + + public function testSegmentedParametersCreatePattern() + { + $router = new Router(new Illuminate\Container\Container); + $router->segments('user', array('firstname', 'lastname')); + $r1 = $router->get('users/{user}', function($user) { return $user['firstname'].$user['lastname']; } ); + $r2 = $router->get('users/{user}/{age}', function($user, $age) { return $user['firstname'].$user['lastname'].$age; } ); + + $this->assertEquals('/users/{firstname}/{lastname}', $r1->getPattern()); + $this->assertEquals('/users/{firstname}/{lastname}/{age}', $r2->getPattern()); + } + + public function testSegmentedParametersCanBeBound() + { + $router = new Router(new Illuminate\Container\Container); + $router->segments('user', array('firstname', 'lastname')); + $router->bind('user', function($value) { return (object)$value; }); + $router->get('users/{user}', function(stdClass $user) { return $user->firstname.$user->lastname; } ); + + $request = Request::create('/users/taylor/otwell', 'GET'); + $this->assertEquals('taylorotwell', $router->dispatch($request)->getContent()); + } + + /** + * @expectedException Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function testSegmentedParametersShouldBeFullySpecified() + { + $router = new Router(new Illuminate\Container\Container); + $router->segments('user', array('firstname', 'lastname')); + $router->bind('user', function($value) { return (object)$value; }); + $router->get('users/{user}', function(stdClass $user) { return $user->firstname.$user->lastname; } ); + + $request = Request::create('/users/taylor', 'GET'); + $this->assertEquals('taylor', $router->dispatch($request)->getContent()); + } + + public function testSegmentedParametersAreProperlySubstituted() + { + $router = new Router(new Illuminate\Container\Container); + $router->segments('user', array('firstname', 'lastname')); + $router->get('users/{user}', function($user) { return $user['firstname'].$user['lastname']; } ); + $router->get('about/{firstname}/{lastname}', function($firstname, $lastname) { return $firstname.$lastname; } ); + + $request = Request::create('/users/taylor/otwell', 'GET'); + $this->assertEquals('taylorotwell', $router->dispatch($request)->getContent()); + + $request = Request::create('/about/taylor/otwell', 'GET'); + $this->assertEquals('taylorotwell', $router->dispatch($request)->getContent()); + } + } class RoutingModelBindingStub {