Skip to content

Commit

Permalink
Adding manipulation of route values,
Browse files Browse the repository at this point in the history
adding and correcting some documentation.
  • Loading branch information
deceze committed Sep 18, 2012
1 parent 3d99f9f commit 6918be6
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 7 deletions.
47 changes: 42 additions & 5 deletions readme.md
Expand Up @@ -126,9 +126,10 @@ This makes your URL structure rather inflexible though. You may eventually decid

$r->add('/foo/\d+:id', array('controller' => 'foos', 'action' => 'view'));

The above route will route the URL `/foo/42` to the same `'controller' => 'foos', 'action' => 'view', 'id' => 42`. Your page will still be hardcoded with the URL `/foo/view/42` all over the place. To solve this and keep your URL structure flexible, use reverse routing, which turns canonical dispatcher information and turns it back into URLs:
The above route will route the URL `/foo/42` to the same `'controller' => 'foos', 'action' => 'view', 'id' => 42`. Your page will still have the hardcoded URL `/foo/view/42` all over the place though. To solve this and keep your URL structure flexible, use reverse routing, which takes canonical dispatcher information and turns it back into URLs:

printf('<a href="%s">Soo foo number 42</a>', $r->reverseRoute(array('controller' => 'foos', 'action' => 'view', 'id' => 42)));
$url = $r->reverseRoute(array('controller' => 'foos', 'action' => 'view', 'id' => 42));
printf('<a href="%s">Soo foo number 42</a>', $url);

The `Router::reverseRoute` method takes a canonical dispatcher information array and spits out a URL, based on the first of your defined routes that matches it:

Expand Down Expand Up @@ -258,14 +259,13 @@ This gives you several different strategies for dealing with non-matches. You ca
die($e->getMessage());
}

This is not recommended, since exceptions are expensive and since a 404 event is not really not an exceptional event, but it may tie in well with your existing error handling strategy.
This is not recommended, since exceptions are expensive and since a 404 event is not really an exceptional event, but it may tie in well with your existing error handling strategy.

It is usually better to pass a callback to `route()` as shown above. Lastly you can also define a catch-all route as your last route and deal with it:

// define regular routes here...

$r->add('/*', array(), 'ErrorHandler::handle404');
$r->route($_GET['url']);

A catch-all route has the advantage that the URL will be parsed and your callback receives a regular `Route` object. This is not the case for callbacks passed to `route()`, which will only receive the non-matched URL as string.

Expand All @@ -279,7 +279,7 @@ You can modify and extend the behavior of Kunststube\Router. The most interestin

$r = new Router(new CaseInsensitiveRouteFactory);

The bulk of the routing logic resides in the `Route` objects. They are the onces parsing the URLs and matching them both ways. If not otherwise specified, the `Router` uses the `RouteFactory` to create new `Route` objects when you call `$r->add(...)`. The default `Route` objects are strictly case sensitive in their matching. An extension of the `Route` class called `CaseInsensitiveRoute` matches URLs and patterns even if their case is different.
The bulk of the routing logic resides in the `Route` objects. They are the ones parsing the URLs and matching them both ways. If not otherwise specified, the `Router` uses the `RouteFactory` to create new `Route` objects when you call `Router::add`. The default `Route` objects are strictly case sensitive in their matching. An extension of the `Route` class called `CaseInsensitiveRoute` matches URLs and patterns even if their case differs.

If you do not want all your routes to be case insensitive but only some, you can create a `CaseInsensitiveRoute` yourself and add it to the routing chain:

Expand All @@ -294,3 +294,40 @@ If you do not want all your routes to be case insensitive but only some, you can
});

$r->route('/Case/INSENSITIVE/rOuTe');


The `Route` class
-----------------

The dispatcher callback will be passed an instance of `Route`. The main purpose of this is to give it access to the matched and parsed values. They can be directly accessed as properties of the object:

function (Route $route) {
echo $route->controller;
echo $route->action;
}

The `Route` object can also be manipulated though and used to generate a new URL according to the set pattern. For example:

$r = new Route('/foo/:id');
$r->id = 42;
echo $r->url(); // /foo/42

This creates a new `Route` object (what is usually done behind the scenes when you call `Router::add`), then sets the missing placeholder `id` to the value `42`, then generates a URL from the set values and the pattern. The values are strictly validated according to the pattern; the following will throw an `InvalidArgumentException`:

$r = new Route('/foo/\d+:id');
$r->id = 'bar'; // invalid value for pattern \d+

Wildcard arguments are supported the same way, but only if the route supports wildcard arguments.

This is mainly useful as efficient way to generate a URL for similar routes. Using `Router::reverseRoute`, all routes must be evaluated in order to find the matching route to generate the correct URL. If you already know the pattern of the URL though and just need to change a single value or two to regenerate the URL, doing so on the correct `Route` object is more efficient:

$r = new Router;
$r->add('/item/\d+:id', array(), function (Route $route) {
echo "Now visiting item {$route->id}. ";
$route->id = $route->id + 1;
echo "The next item is at " . $route->url();
});
$r->route('/item/42'); // Now visiting item 42. The next item is at /item/43

Use this feature with care, since explictly *not* all defined routes are being evaluated and you may get results different from when you'd use reverse routing.

75 changes: 73 additions & 2 deletions route.php
Expand Up @@ -90,7 +90,7 @@ public function matchDispatch(array $comparison) {

foreach ($comparison as $key => $value) {
if (is_string($key) && isset($this->parts[$key])) {
if (preg_match("/^{$this->parts[$key]}\$/", $value)) {
if ($this->matchPart($key, $value)) {
$dispatch[$key] = $value;
} else {
return false;
Expand Down Expand Up @@ -189,7 +189,15 @@ public function wildcardArg($name) {
}

/**
* Returns the matched URL if any.
* @return boolean Whether this route supports wildcard args.
*/
public function supportsWildcardArgs() {
return $this->wildcard;
}

/**
* Returns the last matched URL if any.
* May not be in sync with the current values set on the class if it has been modified.
*
* @return mixed The URL or null if non matched yet.
*/
Expand All @@ -206,6 +214,55 @@ public function pattern() {
return $this->pattern;
}

/**
* Set a dispatch value or wildcard value. If the value is not specified in the pattern, it will be set as wildcard value.
* If the route does not support wildcards, an exception will be thrown.
* If the value does not match the regex defined for the parameter (if any), an exception is thrown.
*
* @param string $name
* @param mixed $value
* @throws InvalidArgumentException if the name/value combintation is invalid for this route.
*/
public function __set($name, $value) {
if (array_key_exists($name, $this->dispatch)) {
$this->setDispatchValue($name, $value);
} else {
$this->setWildcardArg($name, $value);
}
}

/**
* Sets a dispatch value.
* If the value does not match the regex defined for the parameter (if any), an exception is thrown.
*
* @param string $name
* @param mixed $value
* @throws InvalidArgumentException if the name/value combintation is invalid for this route.
*/
public function setDispatchValue($name, $value) {
if (!array_key_exists($name, $this->dispatch)) {
throw new InvalidArgumentException("Route does not specify dispatch value called $name");
}
if (isset($this->parts[$name]) && !$this->matchPart($name, $value)) {
throw new InvalidArgumentException("Value '$value' does not match the rule {$this->parts[$name]} specified for $name");
}
$this->dispatch[$name] = $value;
}

/**
* Set a wildcard value. If the route does not support wildcards, an exception will be thrown.
*
* @param string $name
* @param mixed $value
* @throws InvalidArgumentException if the route does not support wildcards.
*/
public function setWildcardArg($name, $value) {
if (!$this->wildcard) {
throw new InvalidArgumentException("Parameter '$name' not specified in route and route does not allow wildcard arguments");
}
$this->wildcardArgs[$name] = $value;
}


/**
* Initializes the object.
Expand Down Expand Up @@ -260,6 +317,20 @@ protected function parsePart($part) {
return array($match['name'] => $match['pattern']);
}

/**
* Confirms whether a part matches a value.
*
* @param string $name Name of the part, i.e. key from $this->parts.
* @param mixed $value A value to compare to.
* @return boolean
*/
protected function matchPart($name, $value) {
if (!isset($this->parts[$name])) {
throw new LogicException("Part called $name does not exist");
}
return preg_match("/^{$this->parts[$name]}\$/", $value);
}

/**
* Turns an array of parts into a regular expression.
*
Expand Down
46 changes: 46 additions & 0 deletions tests/RouteTest.php
Expand Up @@ -22,4 +22,50 @@ public function testRouteWithoutNames() {
$this->assertInstanceOf('Kunststube\Routing\Route', new Route('/foo/bar'));
}

public function testSettingRouteValues() {
$r = new Route('/:foo/\w+:bar/\d+:baz');
$r->foo = 'foo';
$r->bar = 'bar';
$r->baz = 42;
$this->assertEquals('/foo/bar/42', $r->url());
}

/**
* @expectedException InvalidArgumentException
*/
public function testFailingSettingRouteValues() {
$r = new Route('/:foo/\w+:bar/\d+:baz');
$r->foo = 'foo';
$r->bar = 'bar';
$r->baz = 'baz';
}

public function testSettingWildcardArgs() {
$r = new Route('/:foo/*');
$r->foo = 'foo';
$r->bar = 'bar';
}

/**
* @expectedException InvalidArgumentException
*/
public function testFailingSettingWildcardArgs() {
$r = new Route('/:foo');
$r->foo = 'foo';
$r->bar = 'bar';
}

public function testRouteManipulationAndUrlGeneration() {
$r = new Route('/foo/:bar/*');

$r->bar = 42;
$this->assertEquals('/foo/42', $r->url());

$r->bar = 'baz';
$this->assertEquals('/foo/baz', $r->url());

$r->wild = 'card';
$this->assertEquals('/foo/baz/wild:card', $r->url());
}

}

0 comments on commit 6918be6

Please sign in to comment.