1 <?php
2
3 namespace Alo\Controller;
4
5 use Alo;
6 use Alo\Exception\ControllerException as CE;
7 use ReflectionClass;
8
9 if(!defined('GEN_START')) {
10 http_response_code(404);
11 die();
12 }
13
14 /**
15 * Handles routing to the correct controller and method
16 *
17 * @author Art <a.molcanovas@gmail.com>
18 */
19 class Router {
20
21 /**
22 * Pretty self-explanatory, isn't it?
23 *
24 * @var string
25 */
26 const CONTROLLER_NAMESPACE = '\Controller\\';
27
28 /**
29 * Delimiter used in the regex checking
30 *
31 * @var string
32 */
33 const PREG_DELIMITER = '~';
34 /**
35 * Default params for a route
36 *
37 * @var array
38 */
39 protected static $route_defaults = [
40 'dir' => null,
41 'method' => 'index',
42 'args' => []
43 ];
44 /**
45 * The server name
46 *
47 * @var string
48 */
49 protected $server_name;
50 /**
51 * The server IP
52 *
53 * @var string
54 */
55 protected $server_addr;
56 /**
57 * The port in use
58 *
59 * @var int
60 */
61 protected $port;
62 /**
63 * The remote address
64 *
65 * @var string
66 */
67 protected $remote_addr;
68 /**
69 * The request scheme
70 *
71 * @var string
72 */
73 protected $request_scheme;
74 /**
75 * The raw path info
76 *
77 * @var string
78 */
79 protected $path;
80 /**
81 * Request method in use
82 *
83 * @var string
84 */
85 protected $request_method;
86 /**
87 * Directory name
88 *
89 * @var string
90 */
91 protected $dir;
92 /**
93 * Controller name
94 *
95 * @var string
96 */
97 protected $controller;
98 /**
99 * Method name
100 *
101 * @var string
102 */
103 protected $method;
104 /**
105 * Arguments to pass on to the method
106 *
107 * @var array
108 */
109 protected $method_args;
110 /**
111 * The error controller name
112 *
113 * @var string
114 */
115 protected $err_controller;
116 /**
117 * The default controller
118 *
119 * @var string
120 */
121 protected $default_controller;
122 /**
123 * The routes array
124 *
125 * @var array
126 */
127 protected $routes;
128 /**
129 * Whether we're dealing with a CLI request...
130 *
131 * @var boolean
132 */
133 protected $is_cli_request;
134 /**
135 * Whether we're dealing with an AJAX request
136 *
137 * @var boolean
138 */
139 protected $is_ajax_request;
140
141 /**
142 * Initialises the router
143 *
144 * @author Art <a.molcanovas@gmail.com>
145 * @return Router
146 */
147 function init() {
148 return $this->initNoCall()->tryCall();
149 }
150
151 /**
152 * Tries to call the appropriate class' method
153 *
154 * @author Art <a.molcanovas@gmail.com>
155 * @return Router
156 * @throws CE If the controller is already the error controller
157 * @uses self::forceError()
158 */
159 protected function tryCall() {
160 $rc = $rm = $init = false;
161
162 try {
163 $rc = new ReflectionClass(self::CONTROLLER_NAMESPACE . $this->controller);
164
165 //Must be abstract controller's subclass
166 if(!$rc->isAbstract() &&
167 $rc->isSubclassOf('\Alo\Controller\AbstractController')
168 ) {
169 $rm = $rc->getMethod($this->method);
170
171 //And a public method
172 if($rm->isPublic() && !$rm->isAbstract() && !$rm->isStatic()) {
173 //Excellent. Instantiate!
174 $init = true;
175 } else {
176 $this->forceError();
177 }
178 } else {
179 $this->forceError();
180 }
181 } catch(\ReflectionException $ex) {
182 $this->forceError($ex->getMessage());
183 }
184
185 if($init) {
186 \Log::debug('Initialising controller ' .
187 $this->controller .
188 '->' .
189 $this->method .
190 '(' .
191 implode(',', $this->method_args) .
192 ')');
193 $controller_name = self::CONTROLLER_NAMESPACE . $this->controller;
194 Alo::$controller = new $controller_name;
195 call_user_func_array([Alo::$controller, $this->method], $this->method_args);
196 }
197
198 return $this;
199 }
200
201 /**
202 * Forces the error controller
203 *
204 * @author Art <a.molcanovas@gmail.com>
205 *
206 * @param string $msg Optionally, the error message thrown by ReflectionClass
207 * or ReflectionMethod
208 *
209 * @return \Alo\Controller\Router
210 * @throws CE If the controller is already the error controller
211 * @uses self::tryCall()
212 */
213 protected function forceError($msg = null) {
214 if($this->controller != $this->err_controller) {
215 \Log::debug('404\'d on path: ' .
216 $this->path .
217 '. Settings were as follows: dir: ' .
218 $this->dir .
219 ', class: '
220 .
221 $this->controller .
222 ', method: ' .
223 $this->method .
224 ', args: ' .
225 json_encode($this->method_args));
226
227 $path = DIR_CONTROLLERS . strtolower($this->err_controller) . '.php';
228 if(file_exists($path)) {
229 include_once $path;
230 }
231
232 $this->controller = $this->err_controller;
233 $this->method = 'error';
234 $this->method_args = [404];
235 $this->tryCall();
236 } else {
237 throw new CE('No route available and the error controller '
238 . 'is invalid.' . ($msg ? ' Exception message returned: '
239 . $msg : ''), CE::E_INVALID_ROUTE);
240 }
241
242 return $this;
243 }
244
245 /**
246 * Same as init(), but without attempting to call the controller
247 *
248 * @author Art <a.molcanovas@gmail.com>
249 *
250 * @throws CE When the config file is not found
251 * @throws CE When $error_controller_class is not present in the config file
252 * @throws CE When The default controller is not present in the config file
253 * @throws CE When $routes is not a valid array
254 * @throws CE When a route value is not an array.
255 * @return Router
256 */
257 function initNoCall() {
258 $this->is_cli_request = php_sapi_name() == 'cli' || defined('STDIN');
259 $this->is_ajax_request = \get($_SERVER['HTTP_X_REQUESTED_WITH']) == 'XMLHttpRequest';
260
261 return $this->init_server_vars()
262 ->init_path()
263 ->init_routes()
264 ->resolvePath();
265 }
266
267 /**
268 * Resolves the controller/method path
269 *
270 * @author Art <a.molcanovas@gmail.com>
271 * @return Router
272 */
273 protected function resolvePath() {
274 //Use the default controller if the path is unavailable
275 if(!$this->path) {
276 $filepath = DIR_CONTROLLERS . strtolower($this->default_controller) . '.php';
277
278 if(file_exists($filepath)) {
279 include_once $filepath;
280 }
281
282 $this->controller = $this->default_controller;
283 $this->method = self::$route_defaults['method'];
284 $this->method_args = self::$route_defaults['args'];
285 $this->dir = self::$route_defaults['dir'];
286 } else {
287 $resolved = false;
288
289 //Check if there's a route
290 foreach($this->routes as $source => $dest) {
291 $source_replace = trim(str_replace(self::PREG_DELIMITER, '\\' . self::PREG_DELIMITER, $source), '/');
292 $regex =
293 self::PREG_DELIMITER . '^' . $source_replace . '/?' . '$' . self::PREG_DELIMITER . 'is';
294
295 if(preg_match($regex, $this->path)) {
296 $resolved = true;
297 $explode = explode('/', $this->path);
298
299 $this->dir = $dest['dir'] ? $dest['dir'] . DIRECTORY_SEPARATOR : self::$route_defaults['dir'];
300 $this->controller = isset($dest['class']) ? $dest['class'] : $explode[0];
301
302 //Remove controller
303 array_shift($explode);
304
305 //Set method
306 if($dest['method'] != self::$route_defaults['method']) {
307 $this->method = $dest['method'];
308 } elseif(isset($explode[0])) {
309 $this->method = $explode[0];
310 } else {
311 $this->method = self::$route_defaults['method'];
312 }
313
314 //Remove controller method
315 if(!empty($explode)) {
316 array_shift($explode);
317 }
318
319 //Set preliminary method args
320 if($dest['args'] != self::$route_defaults['args']) {
321 $this->method_args = $dest['args'];
322 } elseif(!empty($explode)) {
323 $this->method_args = $explode;
324 } else {
325 $this->method_args = self::$route_defaults['args'];
326 }
327
328 $replace = explode('/', preg_replace($regex, implode('/', $this->method_args), $this->path));
329
330 //Remove empties
331 foreach($replace as $k => $v) {
332 if($v == '') {
333 unset($replace[$k]);
334 }
335 }
336
337 $this->method_args = $replace;
338
339 break;
340 }
341 }
342
343 if(!$resolved) {
344 //If not, assume the path is controller/method/arg1...
345 $path = explode('/', $this->path);
346
347 $this->dir = null;
348 $this->controller = array_shift($path);
349 $this->method = empty($path) ? self::$route_defaults['method'] : array_shift($path);
350 $this->method_args = $path;
351 }
352
353 $filepath =
354 DIR_CONTROLLERS . str_replace('/', DIRECTORY_SEPARATOR, $this->dir) . $this->controller . '.php';
355
356 if(file_exists($filepath)) {
357 include_once $filepath;
358 }
359 }
360
361 return $this;
362 }
363
364 /**
365 * Initialises the routing variables
366 *
367 * @author Art <a.molcanovas@gmail.com>
368 * @throws CE When the config file is not found
369 * @throws CE When $error_controller_class is not present in the config file
370 * @throws CE When The default controller is not present in the config file
371 * @throws CE When $routes is not a valid array
372 * @throws CE When a route value is not an array.
373 * @return Router
374 */
375 protected function init_routes() {
376 $path = \Alo::loadConfig('router', true);
377
378 if(!file_exists($path)) {
379 throw new CE('Routing config file not found.', CE::E_CONFIG_NOT_FOUND);
380 } else {
381 require $path;
382
383 if(!isset($error_controller_class)) {
384 throw new CE('Error controller class not found in config file.', CE::E_ERR_NOT_FOUND);
385 } elseif(!isset($default_controller)) {
386 throw new CE('$default_controller undefined in config file.', CE::E_DEFAULT_UNDEFINED);
387 } elseif(!is_array(get($routes))) {
388 throw new CE('The routes variable must be an associative array', CE::E_MALFORMED_ROUTES);
389 } else {
390 $this->err_controller = $error_controller_class;
391 $this->default_controller = $default_controller;
392
393 foreach($routes as $k => $v) {
394 if(is_array($v)) {
395 $this->routes[strtolower($k)] = array_merge(self::$route_defaults, $v);
396 } else {
397 throw new CE('Route ' . $k . ' is not a valid array.', CE::E_MALFORMED_ROUTES);
398 }
399 }
400
401 \Log::debug('Routes initialised');
402 }
403 }
404
405 return $this;
406 }
407
408 /**
409 * Initialises the raw path variable
410 *
411 * @author Art <a.molcanovas@gmail.com>
412 * @return Router
413 */
414 protected function init_path() {
415 if(isset($_SERVER['PATH_INFO'])) {
416 $this->path = ltrim($_SERVER['PATH_INFO'], '/');
417 } elseif(isset($_SERVER['argv'])) {
418 //Shift off the "index.php" bit
419 array_shift($_SERVER['argv']);
420 $this->path = join(DIRECTORY_SEPARATOR, $_SERVER['argv']);
421 } else {
422 $this->path = '';
423 }
424
425 $this->path = strtolower($this->path);
426
427 return $this;
428 }
429
430 /**
431 * Initialises most server variables
432 *
433 * @author Art <a.molcanovas@gmail.com>
434 * @return Router
435 */
436 protected function init_server_vars() {
437 $this->port = \get($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : null;
438 $this->remote_addr = \get($_SERVER['REMOTE_ADDR']);
439 $this->request_scheme = \get($_SERVER['REQUEST_SCHEME']);
440 $this->request_method = \get($_SERVER['REQUEST_METHOD']);
441 $this->server_addr = \get($_SERVER['SERVER_ADDR']);
442 $this->server_name = \get($_SERVER['SERVER_NAME']);
443
444 return $this;
445 }
446
447 /**
448 * Returns whether this is a CLI request
449 *
450 * @author Art <a.molcanovas@gmail.com>
451 * @return bool
452 */
453 function is_cli_request() {
454 return $this->is_cli_request;
455 }
456
457 /**
458 * Returns whether this is an AJAX request
459 *
460 * @author Art <a.molcanovas@gmail.com>
461 * @return bool
462 */
463 function is_ajax_request() {
464 return $this->is_ajax_request;
465 }
466
467 /**
468 * Returns the error controller name
469 *
470 * @author Art <a.molcanovas@gmail.com>
471 * @return string
472 */
473 function getErrController() {
474 return $this->err_controller;
475 }
476
477 /**
478 * Returns the controller method name
479 *
480 * @author Art <a.molcanovas@gmail.com>
481 * @return string
482 */
483 function getMethod() {
484 return $this->method;
485 }
486
487 /**
488 * Returns the controller name
489 *
490 * @author Art <a.molcanovas@gmail.com>
491 * @return string
492 */
493 function getController() {
494 return $this->controller;
495 }
496
497 /**
498 * Returns the request port used
499 *
500 * @author Art <a.molcanovas@gmail.com>
501 * @return int
502 */
503 function getPort() {
504 return $this->port;
505 }
506
507 /**
508 * Returns the directory name
509 *
510 * @author Art <a.molcanovas@gmail.com>
511 * @return string
512 */
513 function getDir() {
514 return $this->dir;
515 }
516
517 /**
518 * Returns the request remote IP
519 *
520 * @author Art <a.molcanovas@gmail.com>
521 * @return string
522 */
523 function getRemoteAddr() {
524 return $this->remote_addr;
525 }
526
527 /**
528 * Returns the request method used
529 *
530 * @author Art <a.molcanovas@gmail.com>
531 * @return string
532 */
533 function getRequestMethod() {
534 return $this->request_method;
535 }
536
537 /**
538 * Returns the request scheme used
539 *
540 * @author Art <a.molcanovas@gmail.com>
541 * @return string
542 */
543 function getRequestScheme() {
544 return $this->request_scheme;
545 }
546
547 /**
548 * Returns the server internal IP
549 *
550 * @author Art <a.molcanovas@gmail.com>
551 * @return string
552 */
553 function getServerAddr() {
554 return $this->server_addr;
555 }
556
557 /**
558 * Returns the server name
559 *
560 * @author Art <a.molcanovas@gmail.com>
561 * @return string
562 */
563 function getServerName() {
564 return $this->server_name;
565 }
566
567 /**
568 * Returns the request path
569 *
570 * @author Art <a.molcanovas@gmail.com>
571 * @return string
572 */
573 function getPath() {
574 return $this->path;
575 }
576
577 /**
578 * Returns a string representation of the object data
579 *
580 * @author Art <a.molcanovas@gmail.com>
581 * @return string
582 */
583 function __toString() {
584 return strip_tags(\lite_debug($this));
585 }
586
587 }