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 ($dest['method'] != self::$routeDefaults['method']) {
309 $this->method = $dest['method'];
310 } elseif (isset($explode[0])) {
311 $this->method = $explode[0];
312 } else {
313 $this->method = self::$routeDefaults['method'];
314 }
315
316 //Remove controller method
317 if (!empty($explode)) {
318 array_shift($explode);
319 }
320
321 //Set preliminary method args
322 if ($dest['args'] != self::$routeDefaults['args']) {
323 $this->methodArgs = $dest['args'];
324 } elseif (!empty($explode)) {
325 $this->methodArgs = $explode;
326 } else {
327 $this->methodArgs = self::$routeDefaults['args'];
328 }
329
330 $replace = explode('/', preg_replace($regex, implode('/', $this->methodArgs), $this->path));
331
332 //Remove empties
333 foreach ($replace as $k => $v) {
334 if ($v == '') {
335 unset($replace[$k]);
336 }
337 }
338
339 $this->methodArgs = $replace;
340
341 break;
342 }
343 }
344
345 if (!$resolved) {
346 //If not, assume the path is controller/method/arg1...
347 $path = explode('/', $this->path);
348
349 $this->dir = null;
350 $this->controller = array_shift($path);
351 $this->method = empty($path) ? self::$routeDefaults['method'] : array_shift($path);
352 $this->methodArgs = $path;
353 }
354
355 $filepath =
356 DIR_CONTROLLERS .
357 str_replace('/', DIRECTORY_SEPARATOR, $this->dir) .
358 $this->controller .
359 '.php';
360
361 if (file_exists($filepath)) {
362 include_once $filepath;
363 }
364 }
365
366 return $this;
367 }
368
369 /**
370 * Initialises the routing variables
371 *
372 * @author Art <a.molcanovas@gmail.com>
373 * @throws CE When the config file is not found
374 * @throws CE When $error_controller_class is not present in the config file
375 * @throws CE When The default controller is not present in the config file
376 * @throws CE When $routes is not a valid array
377 * @throws CE When a route value is not an array.
378 * @return Router
379 */
380 protected function initRoutes() {
381 $path = \Alo::loadConfig('router', true);
382
383 if (!file_exists($path)) {
384 throw new CE('Routing config file not found.', CE::E_CONFIG_NOT_FOUND);
385 } else {
386 require $path;
387
388 if (!isset($errorControllerClass)) {
389 throw new CE('Error controller class not found in config file.', CE::E_ERR_NOT_FOUND);
390 } elseif (!isset($defaultController)) {
391 throw new CE('$default_controller undefined in config file.', CE::E_DEFAULT_UNDEFINED);
392 } elseif (!is_array(get($routes))) {
393 throw new CE('The routes variable must be an associative array', CE::E_MALFORMED_ROUTES);
394 } else {
395 $this->errController = $errorControllerClass;
396 $this->defaultController = $defaultController;
397
398 foreach ($routes as $k => $v) {
399 if (is_array($v)) {
400 $this->routes[strtolower($k)] = array_merge(self::$routeDefaults, $v);
401 } else {
402 throw new CE('Route ' . $k . ' is not a valid array.', CE::E_MALFORMED_ROUTES);
403 }
404 }
405
406 \Log::debug('Routes initialised');
407 }
408 }
409
410 return $this;
411 }
412
413 /**
414 * Initialises the raw path variable
415 *
416 * @author Art <a.molcanovas@gmail.com>
417 * @return Router
418 */
419 protected function initPath() {
420 if (isset($_SERVER['PATH_INFO'])) {
421 $this->path = ltrim($_SERVER['PATH_INFO'], '/');
422 } elseif (isset($_SERVER['argv'])) {
423 //Shift off the "index.php" bit
424 array_shift($_SERVER['argv']);
425 $this->path = join(DIRECTORY_SEPARATOR, $_SERVER['argv']);
426 } else {
427 $this->path = '';
428 }
429
430 $this->path = strtolower($this->path);
431
432 return $this;
433 }
434
435 /**
436 * Initialises most server variables
437 *
438 * @author Art <a.molcanovas@gmail.com>
439 * @return Router
440 */
441 protected function initServerVars() {
442 $this->port = \get($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : null;
443 $this->remoteAddr = \get($_SERVER['REMOTE_ADDR']);
444 $this->requestScheme = \get($_SERVER['REQUEST_SCHEME']);
445 $this->requestMethod = \get($_SERVER['REQUEST_METHOD']);
446 $this->serverAddr = \get($_SERVER['SERVER_ADDR']);
447 $this->serverName = \get($_SERVER['SERVER_NAME']);
448
449 return $this;
450 }
451
452 /**
453 * Returns whether this is a CLI request
454 *
455 * @author Art <a.molcanovas@gmail.com>
456 * @return bool
457 */
458 function isCliRequest() {
459 return $this->isCliRequest;
460 }
461
462 /**
463 * Returns whether this is an AJAX request
464 *
465 * @author Art <a.molcanovas@gmail.com>
466 * @return bool
467 */
468 function isAjaxRequest() {
469 return $this->isAjaxRequest;
470 }
471
472 /**
473 * Returns the error controller name
474 *
475 * @author Art <a.molcanovas@gmail.com>
476 * @return string
477 */
478 function getErrController() {
479 return $this->errController;
480 }
481
482 /**
483 * Returns the controller method name
484 *
485 * @author Art <a.molcanovas@gmail.com>
486 * @return string
487 */
488 function getMethod() {
489 return $this->method;
490 }
491
492 /**
493 * Returns the controller name
494 *
495 * @author Art <a.molcanovas@gmail.com>
496 * @return string
497 */
498 function getController() {
499 return $this->controller;
500 }
501
502 /**
503 * Returns the request port used
504 *
505 * @author Art <a.molcanovas@gmail.com>
506 * @return int
507 */
508 function getPort() {
509 return $this->port;
510 }
511
512 /**
513 * Returns the directory name
514 *
515 * @author Art <a.molcanovas@gmail.com>
516 * @return string
517 */
518 function getDir() {
519 return $this->dir;
520 }
521
522 /**
523 * Returns the request remote IP
524 *
525 * @author Art <a.molcanovas@gmail.com>
526 * @return string
527 */
528 function getRemoteAddr() {
529 return $this->remoteAddr;
530 }
531
532 /**
533 * Returns the request method used
534 *
535 * @author Art <a.molcanovas@gmail.com>
536 * @return string
537 */
538 function getRequestMethod() {
539 return $this->requestMethod;
540 }
541
542 /**
543 * Returns the request scheme used
544 *
545 * @author Art <a.molcanovas@gmail.com>
546 * @return string
547 */
548 function getRequestScheme() {
549 return $this->requestScheme;
550 }
551
552 /**
553 * Returns the server internal IP
554 *
555 * @author Art <a.molcanovas@gmail.com>
556 * @return string
557 */
558 function getServerAddr() {
559 return $this->serverAddr;
560 }
561
562 /**
563 * Returns the server name
564 *
565 * @author Art <a.molcanovas@gmail.com>
566 * @return string
567 */
568 function getServerName() {
569 return $this->serverName;
570 }
571
572 /**
573 * Returns the request path
574 *
575 * @author Art <a.molcanovas@gmail.com>
576 * @return string
577 */
578 function getPath() {
579 return $this->path;
580 }
581
582 /**
583 * Returns a string representation of the object data
584 *
585 * @author Art <a.molcanovas@gmail.com>
586 * @return string
587 */
588 function __toString() {
589 return strip_tags(\debugLite($this));
590 }
591
592 }
593 }
594