1 | <?php |
||
2 | namespace Mezon\Router; |
||
3 | |||
4 | // TODO compare speed with klein |
||
5 | // TODO compare speed with Symphony router |
||
6 | // TODO [create|edit:action] |
||
7 | // TODO /date/[i:year]-[i:month]-[i:day] |
||
8 | |||
9 | /** |
||
10 | * Class Router |
||
11 | * |
||
12 | * @package Mezon |
||
13 | * @subpackage Router |
||
14 | * @author Dodonov A.A. |
||
15 | * @version v.1.0 (2019/08/15) |
||
16 | * @copyright Copyright (c) 2019, aeon.org |
||
17 | */ |
||
18 | |||
19 | /** |
||
20 | * Router class |
||
21 | */ |
||
22 | class Router |
||
23 | { |
||
24 | |||
25 | /** |
||
26 | * Mapping of routes to their execution functions for GET requests |
||
27 | * |
||
28 | * @var array |
||
29 | */ |
||
30 | private $getRoutes = []; |
||
31 | |||
32 | /** |
||
33 | * Mapping of routes to their execution functions for GET requests |
||
34 | * |
||
35 | * @var array |
||
36 | */ |
||
37 | private $postRoutes = []; |
||
38 | |||
39 | /** |
||
40 | * Mapping of routes to their execution functions for PUT requests |
||
41 | * |
||
42 | * @var array |
||
43 | */ |
||
44 | private $putRoutes = []; |
||
45 | |||
46 | /** |
||
47 | * Mapping of routes to their execution functions for DELETE requests |
||
48 | * |
||
49 | * @var array |
||
50 | */ |
||
51 | private $deleteRoutes = []; |
||
52 | |||
53 | /** |
||
54 | * Method wich handles invalid route error |
||
55 | * |
||
56 | * @var callable |
||
57 | */ |
||
58 | private $invalidRouteErrorHandler; |
||
59 | |||
60 | /** |
||
61 | * Parsed parameters of the calling router |
||
62 | * |
||
63 | * @var array |
||
64 | */ |
||
65 | protected $parameters = []; |
||
66 | |||
67 | /** |
||
68 | * Method returns request method |
||
69 | * |
||
70 | * @return string Request method |
||
71 | */ |
||
72 | protected function getRequestMethod(): string |
||
73 | { |
||
74 | return $_SERVER['REQUEST_METHOD'] ?? 'GET'; |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * Constructor |
||
79 | */ |
||
80 | public function __construct() |
||
81 | { |
||
82 | $_SERVER['REQUEST_METHOD'] = $this->getRequestMethod(); |
||
83 | |||
84 | $this->invalidRouteErrorHandler = [ |
||
85 | $this, |
||
86 | 'noProcessorFoundErrorHandler' |
||
87 | ]; |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Method fetches actions from the objects and creates GetRoutes for them |
||
92 | * |
||
93 | * @param object $object |
||
94 | * Object to be processed |
||
95 | */ |
||
96 | public function fetchActions(object $object): void |
||
97 | { |
||
98 | $methods = get_class_methods($object); |
||
99 | |||
100 | foreach ($methods as $method) { |
||
101 | if (strpos($method, 'action') === 0) { |
||
102 | $route = \Mezon\Router\Utils::convertMethodNameToRoute($method); |
||
103 | $this->getRoutes["/$route/"] = [ |
||
104 | $object, |
||
105 | $method |
||
106 | ]; |
||
107 | $this->postRoutes["/$route/"] = [ |
||
108 | $object, |
||
109 | $method |
||
110 | ]; |
||
111 | } |
||
112 | } |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * Method adds route and it's handler |
||
117 | * |
||
118 | * $callback function may have two parameters - $route and $parameters. Where $route is a called route, |
||
119 | * and $parameters is associative array (parameter name => parameter value) with URL parameters |
||
120 | * |
||
121 | * @param string $route |
||
122 | * Route |
||
123 | * @param mixed $callback |
||
124 | * Collback wich will be processing route call. |
||
125 | * @param string $requestMethod |
||
126 | * Request type |
||
127 | */ |
||
128 | public function addRoute(string $route, $callback, $requestMethod = 'GET'): void |
||
129 | { |
||
130 | $route = '/' . trim($route, '/') . '/'; |
||
131 | |||
132 | if (is_array($requestMethod)) { |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
133 | foreach ($requestMethod as $r) { |
||
134 | $this->addRoute($route, $callback, $r); |
||
135 | } |
||
136 | } else { |
||
137 | $routes = &$this->_getRoutesForMethod($requestMethod); |
||
138 | // this 'if' is for backward compatibility |
||
139 | // remove it on 02-04-2021 |
||
140 | if (is_array($callback) && isset($callback[1]) && is_array($callback[1])) { |
||
141 | $callback = $callback[1]; |
||
142 | } |
||
143 | $routes[$route] = $callback; |
||
144 | } |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * Method searches route processor |
||
149 | * |
||
150 | * @param mixed $processors |
||
151 | * Callable router's processor |
||
152 | * @param string $route |
||
153 | * Route |
||
154 | * @return mixed Result of the router processor |
||
155 | */ |
||
156 | private function _findStaticRouteProcessor(&$processors, string $route) |
||
157 | { |
||
158 | foreach ($processors as $i => $processor) { |
||
159 | // exact router or 'all router' |
||
160 | if ($i == $route || $i == '/*/') { |
||
161 | if (is_callable($processor) && is_array($processor) === false) { |
||
162 | return $processor($route, []); |
||
163 | } |
||
164 | |||
165 | $functionName = $processor[1] ?? null; |
||
166 | |||
167 | if (is_callable($processor) && |
||
168 | (method_exists($processor[0], $functionName) || isset($processor[0]->$functionName))) { |
||
169 | // passing route path and parameters |
||
170 | return call_user_func($processor, $route, []); |
||
171 | } else { |
||
172 | $callableDescription = \Mezon\Router\Utils::getCallableDescription($processor); |
||
173 | |||
174 | if (isset($processor[0]) && method_exists($processor[0], $functionName) === false) { |
||
175 | throw (new \Exception("'$callableDescription' does not exists")); |
||
176 | } else { |
||
177 | throw (new \Exception("'$callableDescription' must be callable entity")); |
||
178 | } |
||
179 | } |
||
180 | } |
||
181 | } |
||
182 | |||
183 | return false; |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Method returns list of routes for the HTTP method. |
||
188 | * |
||
189 | * @param string $method |
||
190 | * HTTP Method |
||
191 | * @return array Routes |
||
192 | */ |
||
193 | private function &_getRoutesForMethod(string $method): array |
||
194 | { |
||
195 | switch ($method) { |
||
196 | case ('GET'): |
||
197 | $result = &$this->getRoutes; |
||
198 | break; |
||
199 | |||
200 | case ('POST'): |
||
201 | $result = &$this->postRoutes; |
||
202 | break; |
||
203 | |||
204 | case ('PUT'): |
||
205 | $result = &$this->putRoutes; |
||
206 | break; |
||
207 | |||
208 | case ('DELETE'): |
||
209 | $result = &$this->deleteRoutes; |
||
210 | break; |
||
211 | |||
212 | default: |
||
213 | throw (new \Exception('Unsupported request method')); |
||
214 | } |
||
215 | |||
216 | return $result; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Method tries to process static routes without any parameters |
||
221 | * |
||
222 | * @param string $route |
||
223 | * Route |
||
224 | * @return mixed Result of the router processor |
||
225 | */ |
||
226 | private function _tryStaticRoutes($route) |
||
227 | { |
||
228 | $routes = $this->_getRoutesForMethod($this->getRequestMethod()); |
||
229 | |||
230 | return $this->_findStaticRouteProcessor($routes, $route); |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Matching parameter and component |
||
235 | * |
||
236 | * @param mixed $component |
||
237 | * Component of the URL |
||
238 | * @param string $parameter |
||
239 | * Parameter to be matched |
||
240 | * @return string Matched url parameter |
||
241 | */ |
||
242 | private function _matchParameterAndComponent(&$component, string $parameter) |
||
243 | { |
||
244 | $parameterData = explode(':', trim($parameter, '[]')); |
||
245 | $return = ''; |
||
246 | |||
247 | switch ($parameterData[0]) { |
||
248 | case ('i'): |
||
249 | if (is_numeric($component)) { |
||
250 | $component = $component + 0; |
||
251 | $return = $parameterData[1]; |
||
252 | } |
||
253 | break; |
||
254 | case ('a'): |
||
255 | if (preg_match('/^([a-z0-9A-Z_\/\-\.\@]+)$/', $component)) { |
||
256 | $return = $parameterData[1]; |
||
257 | } |
||
258 | break; |
||
259 | case ('il'): |
||
260 | if (preg_match('/^([0-9,]+)$/', $component)) { |
||
261 | $return = $parameterData[1]; |
||
262 | } |
||
263 | break; |
||
264 | case ('s'): |
||
265 | $component = htmlspecialchars($component, ENT_QUOTES); |
||
266 | $return = $parameterData[1]; |
||
267 | break; |
||
268 | default: |
||
269 | throw (new \Exception('Illegal parameter type/value : ' . $parameterData[0])); |
||
270 | } |
||
271 | |||
272 | return $return; |
||
273 | } |
||
274 | |||
275 | /** |
||
276 | * Method matches route and pattern |
||
277 | * |
||
278 | * @param array $cleanRoute |
||
279 | * Cleaned route splitted in parts |
||
280 | * @param array $cleanPattern |
||
281 | * Route pattern |
||
282 | * @return array Array of route's parameters |
||
283 | */ |
||
284 | private function _matchRouteAndPattern(array $cleanRoute, array $cleanPattern) |
||
285 | { |
||
286 | if (count($cleanRoute) !== count($cleanPattern)) { |
||
287 | return false; |
||
0 ignored issues
–
show
|
|||
288 | } |
||
289 | |||
290 | $paremeters = []; |
||
291 | |||
292 | for ($i = 0; $i < count($cleanPattern); $i ++) { |
||
0 ignored issues
–
show
It seems like you are calling the size function
count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}
// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
![]() |
|||
293 | if (\Mezon\Router\Utils::isParameter($cleanPattern[$i])) { |
||
294 | $parameterName = $this->_matchParameterAndComponent($cleanRoute[$i], $cleanPattern[$i]); |
||
295 | |||
296 | // it's a parameter |
||
297 | if ($parameterName !== '') { |
||
298 | // parameter was matched, store it! |
||
299 | $paremeters[$parameterName] = $cleanRoute[$i]; |
||
300 | } else { |
||
301 | return false; |
||
0 ignored issues
–
show
|
|||
302 | } |
||
303 | } else { |
||
304 | // it's a static part of the route |
||
305 | if ($cleanRoute[$i] !== $cleanPattern[$i]) { |
||
306 | return false; |
||
0 ignored issues
–
show
|
|||
307 | } |
||
308 | } |
||
309 | } |
||
310 | |||
311 | $this->parameters = $paremeters; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Method searches dynamic route processor |
||
316 | * |
||
317 | * @param array $processors |
||
318 | * Callable router's processor |
||
319 | * @param string $route |
||
320 | * Route |
||
321 | * @return string|bool Result of the router'scall or false if any error occured |
||
322 | */ |
||
323 | private function _findDynamicRouteProcessor(array &$processors, string $route) |
||
324 | { |
||
325 | $cleanRoute = explode('/', trim($route, '/')); |
||
326 | |||
327 | foreach ($processors as $i => $processor) { |
||
328 | $cleanPattern = explode('/', trim($i, '/')); |
||
329 | |||
330 | if ($this->_matchRouteAndPattern($cleanRoute, $cleanPattern) !== false) { |
||
331 | return call_user_func($processor, $route, $this->parameters); // return result of the router |
||
332 | } |
||
333 | } |
||
334 | |||
335 | return false; |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * Method tries to process dynamic routes with parameters |
||
340 | * |
||
341 | * @param string $route |
||
342 | * Route |
||
343 | * @return string Result of the route call |
||
344 | */ |
||
345 | private function _tryDynamicRoutes(string $route) |
||
346 | { |
||
347 | switch ($this->getRequestMethod()) { |
||
348 | case ('GET'): |
||
349 | $result = $this->_findDynamicRouteProcessor($this->getRoutes, $route); |
||
350 | break; |
||
351 | |||
352 | case ('POST'): |
||
353 | $result = $this->_findDynamicRouteProcessor($this->postRoutes, $route); |
||
354 | break; |
||
355 | |||
356 | case ('PUT'): |
||
357 | $result = $this->_findDynamicRouteProcessor($this->putRoutes, $route); |
||
358 | break; |
||
359 | |||
360 | case ('DELETE'): |
||
361 | $result = $this->_findDynamicRouteProcessor($this->deleteRoutes, $route); |
||
362 | break; |
||
363 | |||
364 | default: |
||
365 | throw (new \Exception('Unsupported request method')); |
||
366 | } |
||
367 | |||
368 | return $result; |
||
0 ignored issues
–
show
|
|||
369 | } |
||
370 | |||
371 | /** |
||
372 | * Method rturns all available routes |
||
373 | */ |
||
374 | private function _getAllRoutesTrace() |
||
375 | { |
||
376 | return (count($this->getRoutes) ? 'GET:' . implode(', ', array_keys($this->getRoutes)) . '; ' : '') . |
||
377 | (count($this->postRoutes) ? 'POST:' . implode(', ', array_keys($this->postRoutes)) . '; ' : '') . |
||
378 | (count($this->putRoutes) ? 'PUT:' . implode(', ', array_keys($this->putRoutes)) . '; ' : '') . |
||
379 | (count($this->deleteRoutes) ? 'DELETE:' . implode(', ', array_keys($this->deleteRoutes)) : ''); |
||
380 | } |
||
381 | |||
382 | /** |
||
383 | * Method processes no processor found error |
||
384 | * |
||
385 | * @param string $route |
||
386 | * Route |
||
387 | */ |
||
388 | public function noProcessorFoundErrorHandler(string $route) |
||
389 | { |
||
390 | throw (new \Exception( |
||
391 | 'The processor was not found for the route ' . $route . ' in ' . $this->_getAllRoutesTrace())); |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Method sets InvalidRouteErrorHandler function |
||
396 | * |
||
397 | * @param callable $function |
||
398 | * Error handler |
||
399 | */ |
||
400 | public function setNoProcessorFoundErrorHandler(callable $function) |
||
401 | { |
||
402 | $oldErrorHandler = $this->invalidRouteErrorHandler; |
||
403 | |||
404 | $this->invalidRouteErrorHandler = $function; |
||
405 | |||
406 | return $oldErrorHandler; |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * Processing specified router |
||
411 | * |
||
412 | * @param string $route |
||
413 | * Route |
||
414 | */ |
||
415 | public function callRoute($route) |
||
416 | { |
||
417 | $route = \Mezon\Router\Utils::prepareRoute($route); |
||
418 | |||
419 | if (($result = $this->_tryStaticRoutes($route)) !== false) { |
||
420 | return $result; |
||
421 | } |
||
422 | |||
423 | if (($result = $this->_tryDynamicRoutes($route)) !== false) { |
||
0 ignored issues
–
show
|
|||
424 | return $result; |
||
425 | } |
||
426 | |||
427 | call_user_func($this->invalidRouteErrorHandler, $route); |
||
428 | } |
||
429 | |||
430 | /** |
||
431 | * Method clears router data. |
||
432 | */ |
||
433 | public function clear() |
||
434 | { |
||
435 | $this->getRoutes = []; |
||
436 | |||
437 | $this->postRoutes = []; |
||
438 | |||
439 | $this->putRoutes = []; |
||
440 | |||
441 | $this->deleteRoutes = []; |
||
442 | } |
||
443 | |||
444 | /** |
||
445 | * Method returns route parameter |
||
446 | * |
||
447 | * @param string $name |
||
448 | * Route parameter |
||
449 | * @return string Route parameter |
||
450 | */ |
||
451 | public function getParam(string $name): string |
||
452 | { |
||
453 | if (isset($this->parameters[$name]) === false) { |
||
454 | throw (new \Exception('Paremeter ' . $name . ' was not found in route', - 1)); |
||
455 | } |
||
456 | |||
457 | return $this->parameters[$name]; |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * Does parameter exists |
||
462 | * |
||
463 | * @param string $name |
||
464 | * Param name |
||
465 | * @return bool True if the parameter exists |
||
466 | */ |
||
467 | public function hasParam(string $name): bool |
||
468 | { |
||
469 | return isset($this->parameters[$name]); |
||
470 | } |
||
471 | |||
472 | /** |
||
473 | * Method returns true if the router exists |
||
474 | * |
||
475 | * @param string $route |
||
476 | * checking route |
||
477 | * @return bool true if the router exists, false otherwise |
||
478 | */ |
||
479 | public function routeExists(string $route): bool |
||
480 | { |
||
481 | $allRoutes = array_merge($this->deleteRoutes, $this->putRoutes, $this->postRoutes, $this->getRoutes); |
||
482 | |||
483 | return isset($allRoutes[$route]); |
||
484 | } |
||
485 | } |
||
486 |