1 | <?php |
||||
2 | |||||
3 | class AltoRouter |
||||
4 | { |
||||
5 | protected $routes = array(); |
||||
6 | protected $namedRoutes = array(); |
||||
7 | protected $basePath = ''; |
||||
8 | protected $matchTypes = array( |
||||
9 | 'i' => '[0-9]++', |
||||
10 | 'a' => '[0-9A-Za-z]++', |
||||
11 | 'h' => '[0-9A-Fa-f]++', |
||||
12 | '*' => '.+?', |
||||
13 | '**' => '.++', |
||||
14 | '' => '[^/\.]++' |
||||
15 | ); |
||||
16 | protected $all = array( |
||||
17 | 'get', 'post' |
||||
18 | ); |
||||
19 | private $server; |
||||
20 | |||||
21 | /** |
||||
22 | * Create router in one call from config. |
||||
23 | * |
||||
24 | * @param array $routes |
||||
25 | * @param string $basePath |
||||
26 | * @param array $matchTypes |
||||
27 | */ |
||||
28 | public function __construct($routes = array(), $basePath = '', $matchTypes = array(), $server = null) |
||||
0 ignored issues
–
show
|
|||||
29 | { |
||||
30 | $this->addRoutes($routes); |
||||
31 | $this->setBasePath($basePath); |
||||
32 | $this->addMatchTypes($matchTypes); |
||||
33 | if(!$server) { |
||||
34 | $this->server = $_SERVER; |
||||
35 | } |
||||
36 | } |
||||
37 | |||||
38 | /** |
||||
39 | * Retrieves all routes. |
||||
40 | * Useful if you want to process or display routes. |
||||
41 | * @return array All routes. |
||||
42 | */ |
||||
43 | public function getRoutes() |
||||
44 | { |
||||
45 | return $this->routes; |
||||
46 | } |
||||
47 | |||||
48 | /** |
||||
49 | * Add multiple routes at once from array in the following format: |
||||
50 | * |
||||
51 | * $routes = array( |
||||
52 | * array($method, $route, $target, $name) |
||||
53 | * ); |
||||
54 | * |
||||
55 | * @param array $routes |
||||
56 | * @return void |
||||
57 | * @author Koen Punt |
||||
58 | */ |
||||
59 | public function addRoutes($routes) |
||||
60 | { |
||||
61 | if (!is_array($routes) && !$routes instanceof Traversable) { |
||||
62 | throw new Exception('Routes should be an array or an instance of Traversable'); |
||||
63 | } |
||||
64 | if(!empty($routes)) { |
||||
65 | foreach ($routes as $route) { |
||||
66 | call_user_func_array(array($this, 'map'), $route); |
||||
67 | } |
||||
68 | } |
||||
69 | } |
||||
70 | |||||
71 | /** |
||||
72 | * Set the base path. |
||||
73 | * Useful if you are running your application from a subdirectory. |
||||
74 | */ |
||||
75 | public function setBasePath($basePath) |
||||
76 | { |
||||
77 | $this->basePath = $basePath; |
||||
78 | } |
||||
79 | |||||
80 | /** |
||||
81 | * Add named match types. It uses array_merge so keys can be overwritten. |
||||
82 | * |
||||
83 | * @param array $matchTypes The key is the name and the value is the regex. |
||||
84 | */ |
||||
85 | public function addMatchTypes($matchTypes) |
||||
86 | { |
||||
87 | $this->matchTypes = array_merge($this->matchTypes, $matchTypes); |
||||
88 | } |
||||
89 | |||||
90 | /** |
||||
91 | * Map a route to a target |
||||
92 | * |
||||
93 | * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE) |
||||
94 | * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] |
||||
95 | * @param mixed $target The target where this route should point to. Can be anything. |
||||
96 | * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. |
||||
97 | */ |
||||
98 | public function map($method, $route, $target, $name = null) |
||||
99 | { |
||||
100 | if ($name) { |
||||
0 ignored issues
–
show
The expression
$name of type null|string is loosely compared to true ; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||||
101 | if (isset($this->namedRoutes[$name])) { |
||||
102 | throw new \Exception("Can not redeclare route '{$name}'"); |
||||
103 | } |
||||
104 | $this->namedRoutes[$name] = $route; |
||||
105 | } |
||||
106 | |||||
107 | $this->routes[] = array($method, $route, $target, $name); |
||||
108 | } |
||||
109 | |||||
110 | /** |
||||
111 | * Reversed routing |
||||
112 | * |
||||
113 | * Generate the URL for a named route. Replace regexes with supplied parameters |
||||
114 | * |
||||
115 | * @param string $routeName The name of the route. |
||||
116 | * @param array @params Associative array of parameters to replace placeholders with. |
||||
117 | * @return string The URL of the route with named parameters in place. |
||||
118 | */ |
||||
119 | public function generate($routeName, array $params = array()) |
||||
120 | { |
||||
121 | |||||
122 | // Check if named route exists |
||||
123 | if (!isset($this->namedRoutes[$routeName])) { |
||||
124 | throw new \Exception("Route '{$routeName}' does not exist."); |
||||
125 | } |
||||
126 | |||||
127 | // Replace named parameters |
||||
128 | $route = $this->namedRoutes[$routeName]; |
||||
129 | |||||
130 | // prepend base path to route url again |
||||
131 | $url = $this->basePath . $route; |
||||
132 | |||||
133 | if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { |
||||
134 | foreach ($matches as $match) { |
||||
135 | $block = $match[0]; |
||||
136 | $pre = $match[1]; |
||||
137 | $param = $match[3]; |
||||
138 | |||||
139 | if ($pre) { |
||||
140 | $block = substr($block, 1); |
||||
141 | } |
||||
142 | |||||
143 | if (isset($params[$param])) { |
||||
144 | $url = str_replace($block, $params[$param], $url); |
||||
145 | } elseif ($match[4]) { |
||||
146 | $url = str_replace($pre . $block, '', $url); |
||||
147 | } |
||||
148 | } |
||||
149 | } |
||||
150 | |||||
151 | return $url; |
||||
152 | } |
||||
153 | |||||
154 | /** |
||||
155 | * Match a given Request Url against stored routes |
||||
156 | * @param string $requestUrl |
||||
157 | * @param string $requestMethod |
||||
158 | * @return array|boolean Array with route information on success, false on failure (no match). |
||||
159 | */ |
||||
160 | public function match($requestUrl = null, $requestMethod = null) |
||||
161 | { |
||||
162 | $params = array(); |
||||
163 | |||||
164 | $requestUrl = $this->getRequestUrl($requestUrl); |
||||
165 | |||||
166 | // set Request Method if it isn't passed as a parameter |
||||
167 | if (is_null($requestMethod)) { |
||||
168 | $requestMethod = $this->server['REQUEST_METHOD']; |
||||
169 | } |
||||
170 | |||||
171 | foreach ($this->routes as $handler) { |
||||
172 | // Method did not match, continue to next route. |
||||
173 | if (!$this->methodMatch($handler[0], $requestMethod, $handler[1], $requestUrl)) { |
||||
174 | continue; |
||||
175 | } |
||||
176 | |||||
177 | return array( |
||||
178 | 'target' => $handler[2], |
||||
179 | 'params' => array_filter($params, function ($k) { return !is_numeric($k); }, ARRAY_FILTER_USE_KEY), |
||||
180 | 'name' => $handler[3] |
||||
181 | ); |
||||
182 | } |
||||
183 | |||||
184 | return false; |
||||
185 | } |
||||
186 | |||||
187 | /** |
||||
188 | * Compile the regex for a given route (EXPENSIVE) |
||||
189 | */ |
||||
190 | private function compileRoute($routeString, $requestUrl) |
||||
191 | { |
||||
192 | $route = $this->getRoute($routeString, $requestUrl); |
||||
193 | |||||
194 | if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { |
||||
195 | $matchTypes = $this->matchTypes; |
||||
196 | foreach ($matches as $match) { |
||||
197 | list($block, $pre, $type, $param, $optional) = $match; |
||||
198 | |||||
199 | if (isset($matchTypes[$type])) { |
||||
200 | $type = $matchTypes[$type]; |
||||
201 | } |
||||
202 | if ($pre === '.') { |
||||
203 | $pre = '\.'; |
||||
204 | } |
||||
205 | |||||
206 | //Older versions of PCRE require the 'P' in (?P<named>) |
||||
207 | $pattern = '(?:' |
||||
208 | . ($pre !== '' ? $pre : null) |
||||
209 | . '(' |
||||
210 | . ($param !== '' ? "?P<$param>" : null) |
||||
211 | . $type |
||||
212 | . '))' |
||||
213 | . ($optional !== '' ? '?' : null); |
||||
214 | |||||
215 | $route = str_replace($block, $pattern, $route); |
||||
216 | } |
||||
217 | } |
||||
218 | |||||
219 | return "`^$route$`u"; |
||||
220 | } |
||||
221 | |||||
222 | /** |
||||
223 | * @param $requestUrl |
||||
224 | * |
||||
225 | * @return mixed |
||||
226 | */ |
||||
227 | private function getRequestUrl($requestUrl) |
||||
228 | { |
||||
229 | // set Request Url if it isn't passed as parameter |
||||
230 | if (is_null($requestUrl)) { |
||||
231 | $requestUrl = parse_url($this->server['REQUEST_URI'], PHP_URL_PATH); |
||||
232 | } |
||||
233 | |||||
234 | return str_replace($this->basePath, '', $requestUrl); |
||||
235 | } |
||||
236 | |||||
237 | /** |
||||
238 | * @param $method |
||||
239 | * @param $requestMethod |
||||
240 | * @param $routeString |
||||
241 | * @param $requestUrl |
||||
242 | * |
||||
243 | * @return mixed |
||||
244 | */ |
||||
245 | private function methodMatch($method, $requestMethod, $routeString, $requestUrl) |
||||
246 | { |
||||
247 | $method = strtolower($method); |
||||
248 | $requestMethod = strtolower($requestMethod); |
||||
249 | $methods = explode('|', $method); |
||||
250 | |||||
251 | if(in_array($requestMethod, $methods)) |
||||
252 | { |
||||
253 | if($routeString == '*') return true; |
||||
254 | |||||
255 | if(is_array($routeString) && !empty($routeString)) { |
||||
256 | if($routeString[0] == '@') { |
||||
257 | return preg_match('`' . substr($routeString, 1) . '`u', $requestUrl, $params); |
||||
0 ignored issues
–
show
$routeString of type array is incompatible with the type string expected by parameter $string of substr() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
258 | } |
||||
259 | $regex = $this->compileRoute($routeString, $requestUrl); |
||||
260 | return preg_match($regex, $requestUrl, $params); |
||||
261 | } |
||||
262 | } |
||||
263 | |||||
264 | return false; |
||||
265 | } |
||||
266 | |||||
267 | /** |
||||
268 | * @param $routeString |
||||
269 | * @param $requestUrl |
||||
270 | * |
||||
271 | * @return bool|string |
||||
272 | */ |
||||
273 | private function getRoute($routeString, $requestUrl) |
||||
274 | { |
||||
275 | $iPointer = $jPointer = 0; |
||||
276 | $nPointer = isset($routeString[0]) ? $routeString[0] : null; |
||||
277 | $regex = $route = false; |
||||
278 | |||||
279 | // Find the longest non-regex substring and match it against the URI |
||||
280 | while (true) { |
||||
281 | if (!isset($routeString[$iPointer])) { |
||||
282 | break; |
||||
283 | } |
||||
284 | if ($regex === false) { |
||||
285 | if(!$this->getRouteRegexCheck($nPointer, $jPointer, $iPointer, $routeString, $requestUrl)) { |
||||
286 | continue; |
||||
287 | } |
||||
288 | $jPointer++; |
||||
289 | } |
||||
290 | $route .= $routeString[$iPointer++]; |
||||
291 | } |
||||
292 | |||||
293 | return $route; |
||||
294 | } |
||||
295 | |||||
296 | private function getRouteRegexCheck($nPointer, $jPointer, $iPointer, $routeString, $requestUrl) |
||||
297 | { |
||||
298 | $cPointer = $nPointer; |
||||
299 | $regex = in_array($cPointer, array('[', '(', '.')); |
||||
300 | if (!$regex && isset($routeString[$iPointer+1])) { |
||||
301 | $nPointer = $routeString[$iPointer + 1]; |
||||
302 | $regex = in_array($nPointer, array('?', '+', '*', '{')); |
||||
303 | } |
||||
304 | if (!$regex && $cPointer !== '/' && (!isset($requestUrl[$jPointer]) || $cPointer !== $requestUrl[$jPointer])) { |
||||
305 | return false; |
||||
306 | } |
||||
307 | return true; |
||||
308 | } |
||||
309 | |||||
310 | public function __call($method, $arguments) |
||||
311 | { |
||||
312 | if(!in_array($method, array('get', 'post', 'delete', 'put', 'patch', 'update', 'all'))) { |
||||
313 | throw new Exception($method . ' not exist in the '. __CLASS__); |
||||
314 | } |
||||
315 | |||||
316 | $methods = $method == 'all' ? implode('|', $this->all) : $method; |
||||
317 | |||||
318 | $route = array_merge(array($methods), $arguments); |
||||
319 | |||||
320 | call_user_func_array(array($this, 'map'), $route); |
||||
321 | } |
||||
322 | |||||
323 | public function __toString() |
||||
324 | { |
||||
325 | return 'AltoRouter'; |
||||
326 | } |
||||
327 | } |
||||
328 |
Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable: