Skip to content

Commit

Permalink
Added route passing functionality.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikecao committed May 10, 2013
1 parent 9fb6b54 commit 28ae5e0
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 100 deletions.
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ Flight::route('GET|POST /', function(){
});
```

Method specific routes have precedence over global routes.

## Regular Expressions

You can use regular expressions in your routes:
Expand Down Expand Up @@ -189,6 +187,24 @@ Flight::route('*', function(){
});
```

## Passing

You can pass execution on to the next matching route by returning `true` from your callback function.

```php
Flight::route('/user/@name', function($name){
// Check some condition
if ($name != "Bob") {
// Continue to next route
return true;
}
});

Flight::route('/user/*', function(){
// This will get called
});
```

# Extending

Flight is designed to be an extensible framework. The framework comes with a set of default methods and components, but it allows you to map your own methods, register your own classes, or even override existing classes and methods.
Expand Down Expand Up @@ -352,7 +368,7 @@ Flight::before('start', function(&$params, &$output){
Flight::before('start', function(&$params, &$output){
echo 'two';

// This will end the chain
// This will end the chain
return false;
});

Expand Down
26 changes: 16 additions & 10 deletions flight/Flight.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,25 +259,31 @@ public static function path($dir) {
* Starts the framework.
*/
public static function _start() {
$router = self::router();
$request = self::request();
$dispatched = false;

// Route the request
$callback = $router->route($request);

if ($callback !== false) {
$params = array_values($router->params);
self::$dispatcher->execute(
$callback,
while ($route = self::router()->route(self::request())) {
$params = array_values($route->params);
$continue = self::$dispatcher->execute(
$route->callback,
$params
);
$dispatched = true;

if ($continue) {
self::router()->next();
}
else {
break;
}
}
else {

if (!$dispatched) {
self::notFound();
}

// Disable caching for AJAX requests
if ($request->ajax) {
if (self::request()->ajax) {
self::response()->cache(false);
}

Expand Down
119 changes: 119 additions & 0 deletions flight/net/Route.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
* Flight: An extensible micro-framework.
*
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/

namespace flight\net;

/**
* The Route class is responsible for routing an HTTP request to
* an assigned callback function. The Router tries to match the
* requested URL against a series of URL patterns.
*/
class Route {
/**
* @var string URL pattern
*/
public $pattern;

/**
* @var mixed Callback function
*/
public $callback;

/**
* @var array HTTP methods
*/
public $methods = array();

/**
* @var array Route parameters
*/
public $params = array();

/**
* @var string Matching regular expression
*/
public $regex;

/**
* Constructor.
*
* @param string $pattern URL pattern
* @param mixed $callback Callback function
* @param array $methods HTTP methods
*/
public function __construct($pattern, $callback, $methods) {
$this->pattern = $pattern;
$this->callback = $callback;
$this->methods = $methods;
}

/**
* Checks if a URL matches the route pattern. Also parses named parameters in the URL.
*
* @param string $url Requested URL
* @return boolean Match status
*/
public function matchUrl($url) {
if ($this->pattern === '*' || $this->pattern === $url) {
return true;
}

$ids = array();
$char = substr($this->pattern, -1);
$this->pattern = str_replace(')', ')?', $this->pattern);

// Build the regex for matching
$regex = preg_replace_callback(
'#@([\w]+)(:([^/\(\)]*))?#',
function($matches) use (&$ids) {
$ids[$matches[1]] = null;
if (isset($matches[3])) {
return '(?P<'.$matches[1].'>'.$matches[3].')';
}
return '(?P<'.$matches[1].'>[^/\?]+)';
},
$this->pattern
);

// Fix trailing slash
if ($char === '/') {
$regex .= '?';
}
// Replace wildcard
else if ($char === '*') {
$regex = str_replace('*', '.+?', $this->pattern);
}
// Allow trailing slash
else {
$regex .= '/?';
}

// Attempt to match route and named parameters
if (preg_match('#^'.$regex.'(?:\?.*)?$#i', $url, $matches)) {
foreach ($ids as $k => $v) {
$this->params[$k] = (array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null;
}

$this->regex = $regex;

return true;
}

return false;
}

/**
* Checks if an HTTP method matches the route methods.
*
* @param string $method HTTP method
* @return bool Match status
*/
public function matchMethod($method) {
return count(array_intersect(array($method, '*'), $this->methods)) > 0;
}
}
116 changes: 34 additions & 82 deletions flight/net/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,11 @@ class Router {
protected $routes = array();

/**
* Matched route.
* Pointer to current route
*
* @var string
* @var int
*/
public $matched = null;

/**
* Matched URL parameters.
*
* @var array
*/
public $params = array();

/**
* Matching regular expression.
*
* @var string
*/
public $regex = null;
protected $index = 0;

/**
* Gets mapped routes.
Expand All @@ -52,7 +38,7 @@ public function getRoutes() {
}

/**
* Resets the router.
* Clears all routes the router.
*/
public function clear() {
$this->routes = array();
Expand All @@ -68,89 +54,55 @@ public function map($pattern, $callback) {
if (strpos($pattern, ' ') !== false) {
list($method, $url) = explode(' ', trim($pattern), 2);

foreach (explode('|', $method) as $value) {
$this->routes[$value][$url] = $callback;
}
$methods = explode('|', $method);

array_push($this->routes, new Route($url, $callback, $methods));
}
else {
$this->routes['*'][$pattern] = $callback;
array_push($this->routes, new Route($pattern, $callback, array('*')));
}
}

/**
* Tries to match a request to a route. Also parses named parameters in the url.
* Routes the current request.
*
* @param string $pattern URL pattern
* @param string $url Requested URL
* @return boolean Match status
* @param Request $request Request object
* @return callable|boolean Matched callback function or false if not found
*/
public function match($pattern, $url) {
$ids = array();
$char = substr($pattern, -1);
$pattern = str_replace(')', ')?', $pattern);

// Build the regex for matching
$regex = preg_replace_callback(
'#@([\w]+)(:([^/\(\)]*))?#',
function($matches) use (&$ids) {
$ids[$matches[1]] = null;
if (isset($matches[3])) {
return '(?P<'.$matches[1].'>'.$matches[3].')';
}
return '(?P<'.$matches[1].'>[^/\?]+)';
},
$pattern
);

// Fix trailing slash
if ($char === '/') {
$regex .= '?';
}
// Replace wildcard
else if ($char === '*') {
$regex = str_replace('*', '.+?', $pattern);
}
// Allow trailing slash
else {
$regex .= '/?';
}

// Attempt to match route and named parameters
if (preg_match('#^'.$regex.'(?:\?.*)?$#i', $url, $matches)) {
foreach ($ids as $k => $v) {
$this->params[$k] = (array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null;
public function route(Request $request) {
while ($route = $this->current()) {
if ($route !== false && $route->matchMethod($request->method) && $route->matchUrl($request->url)) {
return $route;
}

$this->matched = $pattern;
$this->regex = $regex;

return true;
$this->next();
}

return false;
}

/**
* Routes the current request.
* Gets the current route.
*
* @param Request $request Request object
* @return callable|boolean Matched callback function or false if not found
* @return Route
*/
public function route(Request $request) {
$this->matched = null;
$this->regex = null;
$this->params = array();

$routes = isset($this->routes[$request->method]) ? $this->routes[$request->method] : array();
if (isset($this->routes['*'])) $routes += $this->routes['*'];
public function current() {
return isset($this->routes[$this->index]) ? $this->routes[$this->index] : false;
}

foreach ($routes as $pattern => $callback) {
if ($pattern === '*' || $request->url === $pattern || self::match($pattern, $request->url)) {
return $callback;
}
}
/**
* Gets the next route.
*
* @return Route
*/
public function next() {
$this->index++;
}

return false;
/**
* Reset to the first route.
*/
public function reset() {
$this->index = 0;
}
}
?>
10 changes: 5 additions & 5 deletions tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ function ok(){

// Checks if a route was matched
function check($str = 'OK'){
$callback = $this->router->route($this->request);
$params = array_values($this->router->params);
$route = $this->router->route($this->request);
$params = array_values($route->params);

$this->assertTrue(is_callable($callback));
$this->assertTrue(is_callable($route->callback));

call_user_func_array($callback, $params);
call_user_func_array($route->callback, $route->params);

$this->expectOutputString($str);
}
Expand All @@ -53,7 +53,7 @@ function testDefaultRoute(){
}

// Simple path
function testPathRoute() {
function testPathRoute(){
$this->router->map('/path', array($this, 'ok'));
$this->request->url = '/path';

Expand Down

0 comments on commit 28ae5e0

Please sign in to comment.