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