diff --git a/README.md b/README.md index 322c465..92d66bf 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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. @@ -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; }); diff --git a/flight/Flight.php b/flight/Flight.php index 326be9a..70aba8b 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -259,25 +259,31 @@ class Flight { * 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); } diff --git a/flight/net/Route.php b/flight/net/Route.php new file mode 100644 index 0000000..9c501e1 --- /dev/null +++ b/flight/net/Route.php @@ -0,0 +1,119 @@ + + * @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; + } +} \ No newline at end of file diff --git a/flight/net/Router.php b/flight/net/Router.php index df94cc1..606b89f 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -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. @@ -52,7 +38,7 @@ class Router { } /** - * Resets the router. + * Clears all routes the router. */ public function clear() { $this->routes = array(); @@ -68,89 +54,55 @@ class Router { 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; } } ?> diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 8c95d6a..5bcb848 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -34,12 +34,12 @@ class RouterTest extends PHPUnit_Framework_TestCase // 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); } @@ -53,7 +53,7 @@ class RouterTest extends PHPUnit_Framework_TestCase } // Simple path - function testPathRoute() { + function testPathRoute(){ $this->router->map('/path', array($this, 'ok')); $this->request->url = '/path';