Skip to content
This repository has been archived by the owner on Nov 2, 2018. It is now read-only.

Commit

Permalink
Major refactor of Zepto\Router
Browse files Browse the repository at this point in the history
  • Loading branch information
hassankhan committed Jan 29, 2014
1 parent bfaeeec commit 23218ec
Showing 1 changed file with 105 additions and 193 deletions.
298 changes: 105 additions & 193 deletions library/Zepto/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,20 @@

class Router
{
/**
* Contains the callback function to execute, retrieved during run()
*
* @var Closure
*/
protected $callback = null;

/**
* Contains the callback function to execute if none of the given routes can
* be matched to the current URL.
* Request object
*
* @var Closure
* @var Symfony\Component\HttpFoundation\Request
*/
public $error_404 = null;
protected $request;

/**
* An array containing the parameters to pass to the callback function,
* retrieved during run()
* Currently matched route, if one has been set
*
* @var array
* @var Zepto\Route
*/
protected $params = array();
protected $current_route;

/**
* An array containing the list of routing rules and their callback
Expand All @@ -94,102 +86,97 @@ class Router
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 $original_routes = array();

/**
* A sanitized version of the URL, excluding the domain and base component
* Callback function to execute when no matching URL is found
*
* @var string
* @var Closure
*/
protected $url_clean = '';
protected $not_found_handler;

/**
* The dirty URL, direct from $_SERVER['REQUEST_URI']
* Callback function to execute on any other error
*
* @var string
* @var Closure
*/
protected $url_dirty = '';
protected $error_handler;

/**
* Initializes the router by getting the URL and cleaning it.
*
* @param string $url
* @codeCoverageIgnore
*/
public function __construct($url = null)
public function __construct(\Symfony\Component\HttpFoundation\Request $request)
{
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'];
}
}
$this->request = $request;
}

$this->error_404 = $this->set_404_callback();
/**
* Add HTTP GET routes
*
* @see Zepto\Router::route()
* @param string $route
* @param Closure $callback
* @return boolean
*/
public function get($route, \Closure $callback)
{
$this->route(new Route($route, $callback));
// return $this->route($route, $callback, 'GET');
}

// Store the dirty version of the URL
$this->url_dirty = $url;
/**
* Add HTTP POST routes
*
* @see Zepto\Router::route()
* @param string $route
* @param Closure $callback
* @return boolean
*/
public function post($route, \Closure $callback)
{
return $this->route($route, $callback, 'POST');
}

// Clean the URL, removing the protocol, domain, and base directory if there is one
$this->url_clean = $this->__get_clean_url($this->url_dirty);
/**
* Returns a Route object if matching URL is found
*
* @param string $http_method
* @param string $url
* @return Zepto\Route|null
*/
public function match($http_method = 'GET', $url)
{
foreach ($this->routes[$http_method] as $route) {
if ($route->get_url() === $url) {
return $route;
}
}
return null;
}

/**
* Tries to match one of the URL routes to the current URL, otherwise
* execute the default function and return false.
*
* @return array
* @todo See if there's any way of avoiding the triple for-if
*/
public function run()
{
// Whether or not we have matched the URL to a route
$matched_route = false;

// If no routes have been added, then throw an exception
if (!array_key_exists('GET', $this->routes) === true) {
if (empty($this->routes)) {
throw new \Exception('No routes exist in the routing table. Add some');
}

// Sort the array by request method
ksort($this->routes);

// Loop through each request_method level
foreach ($this->routes as $request_method => $routes) {
if ($_SERVER['REQUEST_METHOD'] === $request_method) {
// Loop through each route for this request_method 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;
}
}
$route = $this->match($this->request->getMethod(), $this->request->getPathInfo());

// 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->original_routes[$request_method][$route]);
}
}
}
// Call not found handler if no match was found
if ($route === null) {
$this->not_found();
}
else {
$this->current_route = $route;
$params = array();
call_user_func_array($route->get_callback(), $params);
}
}

Expand All @@ -206,155 +193,80 @@ public function execute()
$this->run();
}
catch (Exception $e) {
echo $e->getMessage();
// Add logging stuff here - maybe?
// Maybe make it do a HTTP 500 error?
}

$this->error_404 = $this->routes['GET']['#^/404/$#'];

if ($this->callback == null || $this->params == null) {
call_user_func($this->error_404);
return false;
}

return call_user_func_array($this->callback, $this->params);
}

/**
* Convenience method for HTTP GET routes* [get description]
* @param string $route
* @param Closure $callback
* @return boolean
* Returns all routes mapped on the routing table.
*
* @return array[Zepto\Route]
*/
public function get($route, \Closure $callback)
public function get_routes()
{
return $this->route($route, $callback, 'GET');
return $this->routes;
}

/**
* Convenience method for HTTP POST routes
* Returns the currently matched route.
*
* @param string $route
* @param Closure $callback
* @return boolean
* @return Zepto\Route
*/
public function post($route, \Closure $callback)
public function get_current_route()
{
return $this->route($route, $callback, 'POST');
return $this->current_route;
}

public function not_found($callback = null)
{
if (is_callable($callback)) {
$this->not_found_handler = $callback;
}
else {
if (is_callable($this->not_found_handler)) {
call_user_func(array($this, 'not_found_handler'));
}
else {
call_user_func(array($this, 'default_not_found_handler'));
}
}
}

/**
* 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 Closure $callback
* @param string $request_method
* @param Route $route
* @param string $request_method
* @return boolean
* @throws Exception If the route already exists in the routing table
*/
public function route($route, \Closure $callback, $request_method = 'GET')
protected function route(Route $route, $http_method = 'GET')
{
// 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: <!var_name>
$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[$request_method][$route])) {
// Does this URL already exist in the routing table?
if (isset($this->routes[$http_method][$route->get_pattern()])) {
// Trigger a new error and exception if errors are on
throw new \Exception('The URI "' . htmlspecialchars($route) . '" already exists in the routing table');
throw new \Exception('The URI {htmlspecialchars($route->get_url())} already exists in the routing table');
}

// Add the route to our routing array
$this->routes[$request_method][$route] = $callback;
$this->original_routes[$request_method][$route] = $original_route;
// Add the route to the routing array
$this->routes[$http_method][$route->get_url()] = $route;

return true;
}

/**
* Returns all routes mapped on the routing table.
*
* @return array
*/
public function get_routes()
protected function default_not_found_handler()
{
return $this->routes;
// Use Twig to do something nice
// Generate a response
echo 'Didn\'t find anything';
}

/**
* Sets the 404 callback. If a callable function is provided as a parameter,
* then that is set as the callback for HTTP 404 errors
*/
public function set_404_callback()
protected function default_error_handler($error = '')
{
// If a callable function is passed to
if (func_num_args() === 1 && gettype(func_get_arg(0)) === 'Closure') {
return $callback;
}
return function() {
echo 'Page doesn\'t exist';
};
echo 'Server error';
}

/**
* 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.
*
* <code>
* http://localhost/projects/test/users///view/1 would return the following,
* assuming that /test/ was the base directory
*
* /users/view/1/
* </code>
*
* @param string $url
* @return string
* @codeCoverageIgnore
*/
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;
}
}

0 comments on commit 23218ec

Please sign in to comment.