-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathrouter.php
131 lines (130 loc) · 7.4 KB
/
router.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<?php
/**
* @author Lloyd Zhou ([email protected])
* A barebones router for PHP. It matches urls and executes PHP functions.
* automatic get variable based on handler function parameter list.
*/
class Router {
protected $prefix = '';
protected $prefix_hook = array();
protected $_tree = array();
protected $_events = array();
protected $_ctypes = array('A' => 'alnum', 'a' => 'alpha', 'd' => 'digit', 'x' => 'xdigit', 'l' => 'lower', 'u' => 'upper');
protected $_default_node = array(self::COLON => array());
const COLON = ':';
const SEPARATOR = '/';
const LEAF = 'LEAF';
public function __construct($tree=array(), $events=array()){
$this->_tree = $tree;
$this->_events = $events;
}
/* helper function to create the tree based on urls, handlers will stored to leaf. */
protected function match_one_path(&$node, $tokens, $cb, $hook){
$token = array_shift($tokens);
$is_token = ($token && self::COLON == $token[0]);
$real_token = $is_token ? substr($token, 1) : $token;
if ($is_token) $node = &$node[self::COLON];
if ($real_token && !array_key_exists($real_token, $node))
$node[$real_token] = $this->_default_node;
if ($real_token)
return $this->match_one_path($node[$real_token], $tokens, $cb, $hook);
$node[self::LEAF] = array($cb, (array)($hook));
}
/* helper function to find handler by $path. */
protected function _resolve($node, $tokens, $params, $depth=0){
$depth = ($depth == 0 && !$tokens[0]) ? 1 : $depth;
$current_token = isset($tokens[$depth])?$tokens[$depth]:'';
if (!$current_token && array_key_exists(self::LEAF, $node))
return array($node[self::LEAF][0], $node[self::LEAF][1], $params);
if (array_key_exists($current_token, $node))
return $this->_resolve($node[$current_token], $tokens, $params, $depth+1);
foreach($node[self::COLON] as $child_token=>$child_node){
/**
* if defined ctype validate function, for the current params, call the ctype function to validate $current_token
* example: "/hello/:name:a.json", and url "/hello/lloyd.json" will call "ctype_alpha" to validate "lloyd"
*/
if ($pos = stripos($child_token, self::COLON)){
if (($m=substr($child_token, $pos+1)) && isset($this->_ctypes[$m]) && !call_user_func('ctype_'.$this->_ctypes[$m], $current_token))
continue;
$child_token = substr($child_token, 0, $pos);
}
/**
* if $current_token not null, and $child_token start with ":"
* set the parameter named $pname and resolve next $path.
* if can not resolve with next $path, restore the parameter named $pname.
*/
$pvalue = array_key_exists($child_token, $params) ? $params[$child_token] : null;
$params[$child_token] = $current_token;
list($cb, $hook, $params) = $this->_resolve($child_node, $tokens, $params, $depth+1);
if ($cb) return array($cb, $hook, $params);
$params[$child_token] = $pvalue;
}
return array(false, '', null);
}
public function resolve($method, $path, $params){
$tokens = explode(self::SEPARATOR, str_replace('.', self::SEPARATOR, $path));
return $this->_resolve(array_key_exists($method, $this->_tree) ? $this->_tree[$method] : $this->_default_node, $tokens, $params);
}
/* API to find handler and execute it by parameters. */
public function execute($params=array(), $method=null, $path=null){
$method = $method ? $method : $_SERVER['REQUEST_METHOD'];
$path = $path ? $path : parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
list($cb, $hook, $params) = $this->resolve($method, $path, $params);
if (!is_callable($cb)) return $this->error(405, "Could not resolve [$method] $path");
/**
* merge the $roter and all $request values into $params.
* auto call the "before" hook before execute the callback handler, and call "after" hook with return value of handler.
* need define the hook with @param $params, and @return $params, so can change it in the hook handler.
* if the hook return false, will trigger 406 error handler.
*/
$input = ((isset($_SERVER['HTTP_CONTENT_TYPE']) && 'application/json' == $_SERVER['HTTP_CONTENT_TYPE'])
|| (isset($_SERVER['CONTENT_TYPE']) && 'application/json' == $_SERVER['CONTENT_TYPE']))
? (array)json_decode(file_get_contents('php://input'), true) : array();
$this->params = array_merge($params, $_SERVER, $_REQUEST, $input, $_FILES, $_COOKIE, isset($_SESSION)?$_SESSION:array(), array('router'=>$this));
foreach(array_merge(array('before'), $hook) as $i=>$h){
if (false === $this->hook($h, $this)) return $this->error(406, "Failed to execute hook: $h");
}
/**
* auto get the variable list based on the callback handler parameter list.
* if the named parameter set in user defined $params or in request, get the value.
* if the named parameter not set, get the default value in callback handler.
*/
$ref = is_array($cb) && isset($cb[1]) ? new ReflectionMethod($cb[0], $cb[1]) : new ReflectionFunction($cb);
$args = $ref->getParameters();
array_walk($args, function(&$p, $i, $params){
$p = isset($params[$p->getName()]) ? $params[$p->getName()] : ($p->isOptional() ? $p->getDefaultValue() : null);
}, $this->params);
/* execute the callback handler and pass the result into "after" hook handler.*/
return $this->hook('after', call_user_func_array($cb, $args), $this);
}
public function match($method, $path, $cb, $hook=null){
foreach((array)($method) as $m){
$m = strtoupper($m);
if (!array_key_exists($m, $this->_tree)) $this->_tree[$m] = $this->_default_node;
foreach((array)($path) as $p){
$tokens = explode(self::SEPARATOR, str_replace('.', self::SEPARATOR, trim($this->prefix.$p, self::SEPARATOR)));
$this->match_one_path($this->_tree[$m], $tokens, $cb, array_merge($this->prefix_hook, (array)$hook));
}
}
return $this;
}
/* register api based on request method. also register "error" and "hook" API. */
public function __call($name, $args){
if (in_array($name, array('get', 'post', 'put', 'patch', 'delete', 'trace', 'connect', 'options', 'head'))
&& array_unshift($args, $name))
return call_user_func_array(array($this, 'match'), $args);
if (in_array($name, array('group', 'prefix'))){
$this->prefix = isset($args[0]) && is_string($args[0]) && self::SEPARATOR == $args[0][0] ? $args[0] : '';
$this->prefix_hook = isset($args[1]) ? (array)$args[1] : array();
}
if (in_array($name, array('error', 'hook'))){
$key = $name. ':'. array_shift($args);
if (isset($args[0]) && is_callable($args[0]))
$this->_events[$key] = $args[0];
else if (isset($this->_events[$key]) && is_callable($this->_events[$key]))
return call_user_func_array($this->_events[$key], $args);
else return ('error' == $name) ? trigger_error('"'.$key.'" not defined to handler error: '.$args[0]) : $args[0];
}
return $this;
}
}