AloFramework documentation
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  • Download

Namespaces

  • Alo
    • Cache
    • CLI
    • Controller
    • Db
    • Exception
    • FileSystem
    • Session
    • Statics
    • Test
    • Validators
    • Windows
  • Controller
  • None
  • PHP

Classes

  • AbstractController
  • AbstractErrorController
  • Router
  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    }
AloFramework documentation API documentation generated by ApiGen 2.8.0