Skip to content

Instantly share code, notes, and snippets.

@meglio
Created February 17, 2013 03:00
Show Gist options
  • Save meglio/4969882 to your computer and use it in GitHub Desktop.
Save meglio/4969882 to your computer and use it in GitHub Desktop.
Simple HTTP routing utility
<?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