Created
February 17, 2013 03:00
-
-
Save meglio/4969882 to your computer and use it in GitHub Desktop.
Simple HTTP routing utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Class Router is static class which serves for routing purposes and can be used in 2 modes: full-map and on-the-go modes. | |
* | |
* Reads request uri path from $_GET['_REQUEST_URI'] | |
*/ | |
class Router | |
{ | |
const GET_VAR = '_REQUEST_URI'; | |
public static function explode($methods, $path) | |
{ | |
self::route($methods, $path); | |
return self::validateRoute($path); | |
} | |
public static function matches($methods, $path) | |
{ | |
self::route($methods, $path); | |
//var_dump(self::$map); die(); | |
return (bool) self::validateRoute($path); | |
} | |
public static function run() | |
{ | |
foreach(self::$map as $path => $meta) | |
if (self::validateRoute($path, 'withCallback') !== null) | |
break; | |
} | |
/** | |
* Tries rule described by $path and returns: | |
* - tokens array, if rule contains @ symbol(s) | |
* - true, if rule is constant string | |
* - null, if rule does not meet | |
* | |
* Call route() for $path before to use this method. | |
* | |
* @param string $path Path of the route, to be key of self::$map | |
* @param bool $runCallback | |
* @throws LogicException | |
* @return mixed|null | |
*/ | |
private static function validateRoute($path, $runCallback = false) | |
{ | |
$path = self::slashTrim($path); | |
if (!array_key_exists($path, self::$map)) | |
throw new LogicException('tryRule call before mapping route path.'); | |
$meta = self::$map[$path]; | |
self::init(); | |
if (!in_array('ALL', $meta['methods']) && !in_array(self::$METHOD, $meta['methods'])) | |
return null; | |
switch ($meta['type']) | |
{ | |
case 'const': | |
if (self::$URI !== $path) | |
return null; | |
$tokens = true; | |
break; | |
case 'var': | |
mb_regex_encoding('UTF-8'); | |
mb_ereg_search_init(self::$URI, $meta['regex']); | |
if (!mb_ereg_search_regs()) | |
return null; | |
$tokens = array_slice(mb_ereg_search_getregs(), 1); | |
break; | |
default: | |
throw new LogicException('Unknown routing rule type'); | |
} | |
if ($runCallback && is_callable($meta['callback'])) | |
call_user_func_array($meta['callback'], $tokens); | |
return $tokens; | |
} | |
/** | |
* Parses route and adds it to self::$map | |
* @param string $methods Examples: 'ALL' , 'GET' , 'GET|POST' | |
* @param string $path Route path; both left and right slashes will be removed automatically | |
* @param callback $callback Standard PHP callback | |
* @param string $name Optional route name (for reverse-routing) | |
*/ | |
public static function route($methods, $path, $callback = null, $name = null) | |
{ | |
$methods = explode('|', trim(strtoupper($methods))); | |
$methods = array_map('trim', $methods); | |
$methods = array_filter($methods, 'strlen'); | |
# Trim both left and right slashes: | |
# x-@/ and x-@ are the same route rules and will match x-10 and x-10/ | |
# Trailing slash at the end in the path specified makes no sense; | |
# if in need for empty token after last slash, we expect x-@/@ | |
$path = self::slashTrim($path); | |
$meta = array('name' => $name, 'methods' => $methods, 'callback' => $callback); | |
if (mb_strpos($path, '@', null, 'UTF-8') === false) | |
$meta['type'] = 'const'; | |
else | |
{ | |
$r = '^'.str_replace('@', '([^/]*?)', preg_quote($path)).'/?$'; | |
$meta = array_merge($meta, array('type' => 'var', 'regex' => $r)); | |
} | |
self::$map[$path] = $meta; | |
} | |
/** | |
* Initializes static fields from request variables. | |
*/ | |
private static function init() | |
{ | |
static $initialized = false; | |
if (!$initialized) | |
{ | |
if (array_key_exists(self::GET_VAR, $_GET)) | |
self::$URI = self::slashTrimL($_GET[self::GET_VAR]); | |
else | |
self::$URI = ''; // direct access to index.php, no redirect applied; or variable passed in GET | |
self::$parts = explode('/', self::$URI); // allowed with UTF-8 strings because it is prefix-free | |
self::$METHOD = strtoupper(trim($_SERVER['REQUEST_METHOD'])); | |
$initialized = true; | |
} | |
} | |
/** | |
* Returns n-th segment of the path or null index is out of range. | |
* Left trailing slash removed from path. | |
* @param $n | |
* @return mixed | |
*/ | |
public static function part($n) | |
{ | |
self::init(); | |
return array_key_exists($n, self::$parts)? self::$parts[$n] : null; | |
} | |
public static function slashTrim($path) | |
{ | |
return self::slashTrimL(self::slashTrimR($path)); | |
} | |
public static function slashTrimR($path) | |
{ | |
$l = mb_strlen($path, 'UTF-8'); | |
if (mb_substr($path, $l-1, 1, 'UTF-8') === '/') | |
$path = mb_substr($path, 0, $l-1, 'UTF-8'); | |
return $path; | |
} | |
public static function slashTrimL($path) | |
{ | |
if (mb_substr($path, 0, 1, 'UTF-8') === '/') | |
$path = mb_substr($path, 1, mb_strlen($path, 'UTF-8')-1, 'UTF-8'); | |
return $path; | |
} | |
/** | |
* Comes from $_SERVER['REQUEST_METHOD'], trimmed and uppercased. | |
* @var string | |
*/ | |
private static $METHOD; | |
/** | |
* Comes from $_GET[self::GET_VAR], left trailing slash delimiter removed. | |
* @var string | |
*/ | |
private static $URI; | |
/** | |
* self::$URI exploded by "/" | |
* @var array | |
*/ | |
private static $parts; | |
/** | |
* Routing map, an array of items with following structure: | |
* ['type' => 'const|var', 'path' => '...', 'methods' => '', 'callback' => ..., 'name' => '...', 'regex' => '...'] | |
* | |
* type = const -> path is a string without @ tokens, simple comparison is possible; | |
* type = var -> path contains @ tokens, regex required | |
* | |
* name - route name; null by default | |
* | |
* regex - only applicable for type=var | |
* | |
* @var array | |
*/ | |
private static $map = array(); | |
public static function _test() | |
{ | |
echo "slashTrimL(''): ".print_r(self::slashTrimL(''), true).'</br>'; | |
echo "slashTrimL('/'): ".print_r(self::slashTrimL('/'), true).'</br>'; | |
echo "slashTrimL('//'): ".print_r(self::slashTrimL('//'), true).'</br>'; | |
echo "slashTrimL('/x'): ".print_r(self::slashTrimL('/x'), true).'</br>'; | |
echo "slashTrimL('/x/'): ".print_r(self::slashTrimL('/x/'), true).'</br>'; | |
echo "slashTrimR('x/y'): ".print_r(self::slashTrimR('x/y'), true).'</br>'; | |
echo "slashTrimR('/x/y/'): ".print_r(self::slashTrimR('/x/y/'), true).'</br>'; | |
echo "slashTrim('/'): ".print_r(self::slashTrim('/'), true).'</br>'; | |
echo "slashTrim('/x/y/'): ".print_r(self::slashTrim('/x/y/'), true).'</br>'; | |
self::_testMethod('GET', 'dashboard', array( | |
array('methodName' => 'matches', 'methods' => 'GET', 'path' => '/dashboard', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'GET', 'path' => 'dashboard', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'GET', 'path' => 'dashboard/', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'GET|POST', 'path' => '/dashboard/', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'ALL', 'path' => 'dashboard', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'ALL', 'path' => '/dashboard/@', 'expected' => false), | |
array('methodName' => 'matches', 'methods' => 'GET', 'path' => 'dashboard-@', 'expected' => false) | |
)); | |
self::_testMethod('GET', 'a/b', array( | |
array('methodName' => 'matches', 'methods' => 'GET', 'path' => '/a/b/', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'ALL', 'path' => 'a/b', 'expected' => true), | |
array('methodName' => 'matches', 'methods' => 'POST|GET|DELETE', 'path' => '/a/b/@', 'expected' => false), | |
array('methodName' => 'matches', 'methods' => 'GET|POST', 'path' => '/dashboard/', 'expected' => false), | |
array('methodName' => 'matches', 'methods' => 'GET', 'path' => '/a/b//', 'expected' => false) | |
)); | |
self::_testMethod('POST', 'category-money/post-10', array( | |
array('methodName' => 'explode', 'methods' => 'POST', 'path' => '/category-@/post-@', 'expected' => array('money', '10')), | |
array('methodName' => 'explode', 'methods' => 'ALL', 'path' => 'category-@/post-@/', 'expected' => array('money', '10')), | |
array('methodName' => 'explode', 'methods' => 'POST|GET|DELETE', 'path' => '/@/post-@', 'expected' => array('category-money', '10')), | |
array('methodName' => 'explode', 'methods' => 'POST', 'path' => 'category-@', 'expected' => null), | |
array('methodName' => 'explode', 'methods' => 'ALL', 'path' => 'category-/post-@', 'expected' => null) | |
)); | |
} | |
private static function _testMethod($method, $uri, $calls) | |
{ | |
self::init(); | |
self::$METHOD = $method; | |
self::$URI = $uri; | |
echo "<h3>REQUEST: ".$method." /".$uri."</h3>"; | |
foreach($calls as $call) | |
{ | |
$res = call_user_func(array('Router', $call['methodName']), $call['methods'], $call['path']); | |
$ok = $res === $call['expected']; | |
echo $call['methodName']."('".$call['methods']."', '".$call['path']."') = ".var_export($res, true); | |
if ($ok) | |
echo ' <span style="color: green">OK</span>'; | |
else | |
echo ' <span style="color: red">ERROR</span>'; | |
echo "<br/>"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment