* @license MIT
*/
// Using the Igniter namespace, you can access the router using \Igniter\Router
namespace Igniter;
use Exception;
/**
* Igniter Router Class
*
* This it the Igniter URL Router, the layer of a web application between the
* URL and the function executed to perform a request. The router determines
* which function to execute for a given URL.
*
*
* $router = new \Igniter\Router;
*
* // Adding a basic route
* $router->route( '/login', 'login_function' );
*
* // Adding a route with a named alphanumeric capture, using the <:var_name> syntax
* $router->route( '/user/view/<:username>', 'view_username' );
*
* // Adding a route with a named numeric capture, using the <#var_name> syntax
* $router->route( '/user/view/<#user_id>', array( 'UserClass', 'view_user' ) );
*
* // Adding a route with a wildcard capture (Including directory separtors), using
* // the <*var_name> syntax
* $router->route( '/browse/<*categories>', 'category_browse' );
*
* // Adding a wildcard capture (Excludes directory separators), using the
* // syntax
* $router->route( '/browse/', 'browse_category' );
*
* // Adding a custom regex capture using the <:var_name|regex> syntax
* $router->route( '/lookup/zipcode/<:zipcode|[0-9]{5}>', 'zipcode_func' );
*
* // Specifying priorities
* $router->route( '/users/all', 'view_users', 1 ); // Executes first
* $router->route( '/users/<:status>', 'view_users_by_status', 100 ); // Executes after
*
* // Specifying a default callback function if no other route is matched
* $router->default_route( 'page_404' );
*
* // Run the router
* $router->execute();
*
*
* @since 2.0.0
*/
class Router
{
/**
* Contains the callback function to execute, retrieved during run()
*
* @var string|array
*/
protected $callback = null;
/**
* Contains the callback function to execute if none of the given routes can
* be matched to the current URL.
*
* @var atring|array
*/
protected $default_route = null;
/**
* Contains the last route executed, used when chaining methods calls in
* the route() function (Such as for put(), post(), and delete()).
*
* @var pointer
*/
protected $last_route = null;
/**
* An array containing the parameters to pass to the callback function,
* retrieved during run()
*
* @var array
*/
protected $params = array();
/**
* An array containing the list of routing rules and their callback
* functions, as well as their priority and any additional paramters.
*
* @var array
*/
protected $routes = array();
/**
* An array containing the list of routing rules before they are parsed
* into their regex equivalents, used for debugging and test cases
*
* @var array
*/
protected $routes_original = array();
/**
* Whether or not to display errors for things like malformed routes or
* conflicting routes.
*
* @var boolean
*/
protected $show_errors = true;
/**
* A sanitized version of the URL, excluding the domain and base component
*
* @var string
*/
protected $url_clean = '';
/**
* The dirty URL, direct from $_SERVER['REQUEST_URI']
*
* @var string
*/
protected $url_dirty = '';
/**
* Initializes the router by getting the URL and cleaning it.
*
* @param string $url
*/
public function __construct($url = null)
{
if ($url == null) {
// Get the current URL, differents depending on platform/server software
if (!empty($_SERVER['REQUEST_URL'])) {
$url = $_SERVER['REQUEST_URL'];
} else {
$url = $_SERVER['REQUEST_URI'];
}
}
// Store the dirty version of the URL
$this->url_dirty = $url;
// Clean the URL, removing the protocol, domain, and base directory if there is one
$this->url_clean = $this->__get_clean_url($this->url_dirty);
}
/**
* Enables the display of errors such as malformed URL routing rules or
* conflicting routing rules. Not recommended for production sites.
*
* @return self
*/
public function show_errors()
{
$this->show_errors = true;
return $this;
}
/**
* Disables the display of errors such as malformed URL routing rules or
* conflicting routing rules. Not recommended for production sites.
*
* @return self
*/
public function hide_errors()
{
$this->show_errors = false;
return $this;
}
/**
* If the router cannot match the current URL to any of the given routes,
* the function passed to this method will be executed instead. This would
* be useful for displaying a 404 page for example.
*
* @param Callable $callback
* @return self
*/
public function default_route($callback)
{
$this->default_route = $callback;
return $this;
}
/**
* Tries to match one of the URL routes to the current URL, otherwise
* execute the default function and return false.
*
* @return boolean
*/
public function run()
{
// Whether or not we have matched the URL to a route
$matched_route = false;
// Sort the array by priority
ksort($this->routes);
// Loop through each priority level
foreach ($this->routes as $priority => $routes) {
// Loop through each route for this priority level
foreach ($routes as $route => $callback) {
// Does the routing rule match the current URL?
if (preg_match($route, $this->url_clean, $matches)) {
// A routing rule was matched
$matched_route = TRUE;
// Parameters to pass to the callback function
$params = array($this->url_clean);
// Get any named parameters from the route
foreach ($matches as $key => $match) {
if (is_string($key)) {
$params[] = $match;
}
}
// Store the parameters and callback function to execute later
$this->params = $params;
$this->callback = $callback;
// Return the callback and params, useful for unit testing
return array('callback' => $callback, 'params' => $params, 'route' => $route, 'original_route' => $this->routes_original[$priority][$route]);
}
}
}
// Was a match found or should we execute the default callback?
if (!$matched_route && $this->default_route !== null) {
return array('params' => $this->url_clean, 'callback' => $this->default_route, 'route' => false, 'original_route' => false);
}
}
/**
* Calls the appropriate callback function and passes the given parameters
* given by Router::run()
*
* @return boolean
*/
public function dispatch()
{
if ($this->callback == null || $this->params == null) {
throw new Exception('No callback or parameters found, please run $router->run() before $router->dispatch()');
return false;
}
call_user_func_array($this->callback, $this->params);
return true;
}
/**
* Runs the router matching engine and then calls the dispatcher
*
* @uses Router::run()
* @uses Router::dispatch()
*/
public function execute()
{
$this->run();
$this->dispatch();
}
/**
* Adds a new URL routing rule to the routing table, after converting any of
* our special tokens into proper regular expressions.
*
* @param string $route
* @param Callable $callback
* @param integer $priority
* @return boolean
*/
public function route($route, $callback, $priority = 10)
{
// Keep the original routing rule for debugging/unit tests
$original_route = $route;
// Make sure the route ends in a / since all of the URLs will
$route = rtrim($route, '/') . '/';
// Custom capture, format: <:var_name|regex>
$route = preg_replace('/\<\:(.*?)\|(.*?)\>/', '(?P<\1>\2)', $route);
// Alphanumeric capture (0-9A-Za-z-_), format: <:var_name>
$route = preg_replace('/\<\:(.*?)\>/', '(?P<\1>[A-Za-z0-9\-\_]+)', $route);
// Numeric capture (0-9), format: <#var_name>
$route = preg_replace('/\<\#(.*?)\>/', '(?P<\1>[0-9]+)', $route);
// Wildcard capture (Anything INCLUDING directory separators), format: <*var_name>
$route = preg_replace('/\<\*(.*?)\>/', '(?P<\1>.+)', $route);
// Wildcard capture (Anything EXCLUDING directory separators), format:
$route = preg_replace('/\<\!(.*?)\>/', '(?P<\1>[^\/]+)', $route);
// Add the regular expression syntax to make sure we do a full match or no match
$route = '#^' . $route . '$#';
// Does this URL routing rule already exist in the routing table?
if (isset($this->routes[$priority][$route])) {
// Trigger a new error and exception if errors are on
if ($this->show_errors) {
throw new Exception('The URI "' . htmlspecialchars($route) . '" already exists in the router table');
}
return false;
}
// Add the route to our routing array
$this->routes[$priority][$route] = $callback;
$this->routes_original[$priority][$route] = $original_route;
return true;
}
/**
* Retrieves the part of the URL after the base (Calculated from the location
* of the main application file, such as index.php), excluding the query
* string. Adds a trailing slash.
*
*
* http://localhost/projects/test/users///view/1 would return the following,
* assuming that /test/ was the base directory
*
* /users/view/1/
*
*
* @param string $url
* @return string
*/
protected function __get_clean_url($url)
{
// The request url might be /project/index.php, this will remove the /project part
$url = str_replace(dirname($_SERVER['SCRIPT_NAME']), '', $url);
// Remove the query string if there is one
$query_string = strpos($url, '?');
if ($query_string !== false) {
$url = substr($url, 0, $query_string);
}
// If the URL looks like http://localhost/index.php/path/to/folder remove /index.php
if (substr($url, 1, strlen(basename($_SERVER['SCRIPT_NAME']))) == basename($_SERVER['SCRIPT_NAME'])) {
$url = substr($url, strlen(basename($_SERVER['SCRIPT_NAME'])) + 1);
}
// Make sure the URI ends in a /
$url = rtrim($url, '/') . '/';
// Replace multiple slashes in a url, such as /my//dir/url
$url = preg_replace('/\/+/', '/', $url);
return $url;
}
}