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 ) {
174 $rm = $rc->getMethod($this->method);
175
176 //And a public method
177 if ($rm->isPublic() && !$rm->isAbstract() && !$rm->isStatic()) {
178 //Excellent. Instantiate!
179 $init = true;
180 } else {
181 $this->forceError();
182 }
183 } else {
184 $this->forceError();
185 }
186 } catch (\ReflectionException $ex) {
187 $this->forceError($ex->getMessage());
188 }
189
190 if ($init) {
191 \Log::debug('Initialising controller ' . $this->controller . '->' . $this->method . '(' .
192 implode(',', $this->methodArgs) . ')');
193 $controllerName = self::CONTROLLER_NAMESPACE . $this->controller;
194 Alo::$controller = new $controllerName;
195 call_user_func_array([Alo::$controller, $this->method], $this->methodArgs);
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->errController) {
215 \Log::debug('404\'d on path: ' . $this->path . '. Settings were as follows: dir: ' . $this->dir .
216 ', class: ' . $this->controller . ', method: ' . $this->method . ', args: ' .
217 json_encode($this->methodArgs));
218
219 $path = DIR_CONTROLLERS . strtolower($this->errController) . '.php';
220 if (file_exists($path)) {
221 include_once $path;
222 }
223
224 $this->controller = $this->errController;
225 $this->method = 'error';
226 $this->methodArgs = [404];
227 $this->tryCall();
228 } else {
229 throw new CE('No route available and the error controller ' . 'is invalid.' .
230 ($msg ? ' Exception message returned: ' . $msg : ''), CE::E_INVALID_ROUTE);
231 }
232
233 return $this;
234 }
235
236 /**
237 * Same as init(), but without attempting to call the controller
238 *
239 * @author Art <a.molcanovas@gmail.com>
240 *
241 * @throws CE When the config file is not found
242 * @throws CE When $error_controller_class is not present in the config file
243 * @throws CE When The default controller is not present in the config file
244 * @throws CE When $routes is not a valid array
245 * @throws CE When a route value is not an array.
246 * @return Router
247 */
248 function initNoCall() {
249 $this->isCliRequest = php_sapi_name() == 'cli' || defined('STDIN');
250 $this->isAjaxRequest = \get($_SERVER['HTTP_X_REQUESTED_WITH']) == 'XMLHttpRequest';
251
252 return $this->initServerVars()->initPath()->initRoutes()->resolvePath();
253 }
254
255 /**
256 * Resolves the controller/method path
257 *
258 * @author Art <a.molcanovas@gmail.com>
259 * @return Router
260 */
261 protected function resolvePath() {
262 //Use the default controller if the path is unavailable
263 if (!$this->path) {
264 $filepath = DIR_CONTROLLERS . strtolower($this->defaultController) . '.php';
265
266 if (file_exists($filepath)) {
267 include_once $filepath;
268 }
269
270 $this->controller = $this->defaultController;
271 $this->method = self::$routeDefaults['method'];
272 $this->methodArgs = self::$routeDefaults['args'];
273 $this->dir = self::$routeDefaults['dir'];
274 } else {
275 $resolved = false;
276
277 //Check if there's a route
278 foreach ($this->routes as $source => $dest) {
279 $sourceReplace =
280 trim(str_replace(self::PREG_DELIMITER, '\\' . self::PREG_DELIMITER, $source), '/');
281 $regex =
282 self::PREG_DELIMITER . '^' . $sourceReplace . '/?' . '$' . self::PREG_DELIMITER . 'is';
283
284 if (preg_match($regex, $this->path)) {
285 $resolved = true;
286 $explode = explode('/', $this->path);
287
288 $this->dir =
289 $dest['dir'] ? $dest['dir'] . DIRECTORY_SEPARATOR : self::$routeDefaults['dir'];
290 $this->controller = isset($dest['class']) ? $dest['class'] : $explode[0];
291
292 //Remove controller
293 array_shift($explode);
294
295 //Set method
296 if ($dest['method'] != self::$routeDefaults['method']) {
297 $this->method = $dest['method'];
298 } elseif (isset($explode[0])) {
299 $this->method = $explode[0];
300 } else {
301 $this->method = self::$routeDefaults['method'];
302 }
303
304 //Remove controller method
305 if (!empty($explode)) {
306 array_shift($explode);
307 }
308
309 //Set preliminary method args
310 if ($dest['args'] != self::$routeDefaults['args']) {
311 $this->methodArgs = $dest['args'];
312 } elseif (!empty($explode)) {
313 $this->methodArgs = $explode;
314 } else {
315 $this->methodArgs = self::$routeDefaults['args'];
316 }
317
318 $replace = explode('/', preg_replace($regex, implode('/', $this->methodArgs), $this->path));
319
320 //Remove empties
321 foreach ($replace as $k => $v) {
322 if ($v == '') {
323 unset($replace[$k]);
324 }
325 }
326
327 $this->methodArgs = $replace;
328
329 break;
330 }
331 }
332
333 if (!$resolved) {
334 //If not, assume the path is controller/method/arg1...
335 $path = explode('/', $this->path);
336
337 $this->dir = null;
338 $this->controller = array_shift($path);
339 $this->method = empty($path) ? self::$routeDefaults['method'] : array_shift($path);
340 $this->methodArgs = $path;
341 }
342
343 $filepath =
344 DIR_CONTROLLERS . str_replace('/', DIRECTORY_SEPARATOR, $this->dir) . $this->controller .
345 '.php';
346
347 if (file_exists($filepath)) {
348 include_once $filepath;
349 }
350 }
351
352 return $this;
353 }
354
355 /**
356 * Initialises the routing variables
357 *
358 * @author Art <a.molcanovas@gmail.com>
359 * @throws CE When the config file is not found
360 * @throws CE When $error_controller_class is not present in the config file
361 * @throws CE When The default controller is not present in the config file
362 * @throws CE When $routes is not a valid array
363 * @throws CE When a route value is not an array.
364 * @return Router
365 */
366 protected function initRoutes() {
367 $path = \Alo::loadConfig('router', true);
368
369 if (!file_exists($path)) {
370 throw new CE('Routing config file not found.', CE::E_CONFIG_NOT_FOUND);
371 } else {
372 require $path;
373
374 if (!isset($errorControllerClass)) {
375 throw new CE('Error controller class not found in config file.', CE::E_ERR_NOT_FOUND);
376 } elseif (!isset($defaultController)) {
377 throw new CE('$default_controller undefined in config file.', CE::E_DEFAULT_UNDEFINED);
378 } elseif (!is_array(get($routes))) {
379 throw new CE('The routes variable must be an associative array', CE::E_MALFORMED_ROUTES);
380 } else {
381 $this->errController = $errorControllerClass;
382 $this->defaultController = $defaultController;
383
384 foreach ($routes as $k => $v) {
385 if (is_array($v)) {
386 $this->routes[strtolower($k)] = array_merge(self::$routeDefaults, $v);
387 } else {
388 throw new CE('Route ' . $k . ' is not a valid array.', CE::E_MALFORMED_ROUTES);
389 }
390 }
391
392 \Log::debug('Routes initialised');
393 }
394 }
395
396 return $this;
397 }
398
399 /**
400 * Initialises the raw path variable
401 *
402 * @author Art <a.molcanovas@gmail.com>
403 * @return Router
404 */
405 protected function initPath() {
406 if (isset($_SERVER['PATH_INFO'])) {
407 $this->path = ltrim($_SERVER['PATH_INFO'], '/');
408 } elseif (isset($_SERVER['argv'])) {
409 //Shift off the "index.php" bit
410 array_shift($_SERVER['argv']);
411 $this->path = join(DIRECTORY_SEPARATOR, $_SERVER['argv']);
412 } else {
413 $this->path = '';
414 }
415
416 $this->path = strtolower($this->path);
417
418 return $this;
419 }
420
421 /**
422 * Initialises most server variables
423 *
424 * @author Art <a.molcanovas@gmail.com>
425 * @return Router
426 */
427 protected function initServerVars() {
428 $this->port = \get($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : null;
429 $this->remoteAddr = \get($_SERVER['REMOTE_ADDR']);
430 $this->requestScheme = \get($_SERVER['REQUEST_SCHEME']);
431 $this->requestMethod = \get($_SERVER['REQUEST_METHOD']);
432 $this->serverAddr = \get($_SERVER['SERVER_ADDR']);
433 $this->serverName = \get($_SERVER['SERVER_NAME']);
434
435 return $this;
436 }
437
438 /**
439 * Returns whether this is a CLI request
440 *
441 * @author Art <a.molcanovas@gmail.com>
442 * @return bool
443 */
444 function isCliRequest() {
445 return $this->isCliRequest;
446 }
447
448 /**
449 * Returns whether this is an AJAX request
450 *
451 * @author Art <a.molcanovas@gmail.com>
452 * @return bool
453 */
454 function isAjaxRequest() {
455 return $this->isAjaxRequest;
456 }
457
458 /**
459 * Returns the error controller name
460 *
461 * @author Art <a.molcanovas@gmail.com>
462 * @return string
463 */
464 function getErrController() {
465 return $this->errController;
466 }
467
468 /**
469 * Returns the controller method name
470 *
471 * @author Art <a.molcanovas@gmail.com>
472 * @return string
473 */
474 function getMethod() {
475 return $this->method;
476 }
477
478 /**
479 * Returns the controller name
480 *
481 * @author Art <a.molcanovas@gmail.com>
482 * @return string
483 */
484 function getController() {
485 return $this->controller;
486 }
487
488 /**
489 * Returns the request port used
490 *
491 * @author Art <a.molcanovas@gmail.com>
492 * @return int
493 */
494 function getPort() {
495 return $this->port;
496 }
497
498 /**
499 * Returns the directory name
500 *
501 * @author Art <a.molcanovas@gmail.com>
502 * @return string
503 */
504 function getDir() {
505 return $this->dir;
506 }
507
508 /**
509 * Returns the request remote IP
510 *
511 * @author Art <a.molcanovas@gmail.com>
512 * @return string
513 */
514 function getRemoteAddr() {
515 return $this->remoteAddr;
516 }
517
518 /**
519 * Returns the request method used
520 *
521 * @author Art <a.molcanovas@gmail.com>
522 * @return string
523 */
524 function getRequestMethod() {
525 return $this->requestMethod;
526 }
527
528 /**
529 * Returns the request scheme used
530 *
531 * @author Art <a.molcanovas@gmail.com>
532 * @return string
533 */
534 function getRequestScheme() {
535 return $this->requestScheme;
536 }
537
538 /**
539 * Returns the server internal IP
540 *
541 * @author Art <a.molcanovas@gmail.com>
542 * @return string
543 */
544 function getServerAddr() {
545 return $this->serverAddr;
546 }
547
548 /**
549 * Returns the server name
550 *
551 * @author Art <a.molcanovas@gmail.com>
552 * @return string
553 */
554 function getServerName() {
555 return $this->serverName;
556 }
557
558 /**
559 * Returns the request path
560 *
561 * @author Art <a.molcanovas@gmail.com>
562 * @return string
563 */
564 function getPath() {
565 return $this->path;
566 }
567
568 /**
569 * Returns a string representation of the object data
570 *
571 * @author Art <a.molcanovas@gmail.com>
572 * @return string
573 */
574 function __toString() {
575 return strip_tags(\debugLite($this));
576 }
577
578 }
579 }
580