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

Namespaces

  • Alo
    • Cache
    • Controller
    • Db
    • Exception
    • Session
    • Statics
    • Test
    • Validators
  • 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       /**
 36        * The server name
 37        *
 38        * @var string
 39        */
 40       protected $server_name;
 41 
 42       /**
 43        * The server IP
 44        *
 45        * @var string
 46        */
 47       protected $server_addr;
 48 
 49       /**
 50        * The port in use
 51        *
 52        * @var int
 53        */
 54       protected $port;
 55 
 56       /**
 57        * The remote address
 58        *
 59        * @var string
 60        */
 61       protected $remote_addr;
 62 
 63       /**
 64        * The request scheme
 65        *
 66        * @var string
 67        */
 68       protected $request_scheme;
 69 
 70       /**
 71        * The raw path info
 72        *
 73        * @var string
 74        */
 75       protected $path;
 76 
 77       /**
 78        * Request method in use
 79        *
 80        * @var string
 81        */
 82       protected $request_method;
 83 
 84       /**
 85        * Directory name
 86        *
 87        * @var string
 88        */
 89       protected $dir;
 90 
 91       /**
 92        * Controller name
 93        *
 94        * @var string
 95        */
 96       protected $controller;
 97 
 98       /**
 99        * Method name
100        *
101        * @var string
102        */
103       protected $method;
104 
105       /**
106        * Arguments to pass on to the method
107        *
108        * @var array
109        */
110       protected $method_args;
111 
112       /**
113        * The error controller name
114        *
115        * @var string
116        */
117       protected $err_controller;
118 
119       /**
120        * The default controller
121        *
122        * @var string
123        */
124       protected $default_controller;
125 
126       /**
127        * The routes array
128        *
129        * @var array
130        */
131       protected $routes;
132 
133       /**
134        * Whether we're dealing with a CLI request...
135        *
136        * @var boolean
137        */
138       protected $is_cli_request;
139 
140       /**
141        * Whether we're dealing with an AJAX request
142        *
143        * @var boolean
144        */
145       protected $is_ajax_request;
146 
147       /**
148        * Default params for a route
149        *
150        * @var array
151        */
152       protected static $route_defaults = [
153          'dir'    => null,
154          'method' => 'index',
155          'args'   => []
156       ];
157 
158       /**
159        * Initialises the router
160        *
161        * @author Art <a.molcanovas@gmail.com>
162        * @return Router
163        */
164       function init() {
165          return $this->initNoCall()->tryCall();
166       }
167 
168       /**
169        * Same as init(), but without attempting to call the controller
170        *
171        * @author Art <a.molcanovas@gmail.com>
172        * @return Router
173        */
174       function initNoCall() {
175          $this->is_cli_request = php_sapi_name() == 'cli' || defined('STDIN');
176          $this->is_ajax_request = \get($_SERVER['HTTP_X_REQUESTED_WITH']) == 'XMLHttpRequest';
177 
178          return $this->init_server_vars()
179             ->init_path()
180             ->init_routes()
181             ->resolvePath();
182       }
183 
184       /**
185        * Returns whether this is a CLI request
186        *
187        * @author Art <a.molcanovas@gmail.com>
188        * @return bool
189        */
190       function is_cli_request() {
191          return $this->is_cli_request;
192       }
193 
194       /**
195        * Returns whether this is an AJAX request
196        *
197        * @author Art <a.molcanovas@gmail.com>
198        * @return bool
199        */
200       function is_ajax_request() {
201          return $this->is_ajax_request;
202       }
203 
204       /**
205        * Forces the error controller
206        *
207        * @author Art <a.molcanovas@gmail.com>
208        * @param string $msg Optionally, the error message thrown by ReflectionClass
209        *                    or ReflectionMethod
210        * @return \Alo\Controller\Router
211        * @throws CE If the controller is already the error controller
212        * @uses   self::tryCall()
213        */
214       protected function forceError($msg = null) {
215          if ($this->controller != $this->err_controller) {
216             \Log::debug('404\'d on path: ' . $this->path . '. Settings were as follows: dir: ' . $this->dir . ', class: '
217                . $this->controller . ', method: ' . $this->method . ', args: ' . json_encode($this->method_args));
218 
219             $path = DIR_CONTROLLERS . strtolower($this->err_controller) . '.php';
220             if (file_exists($path)) {
221                include_once $path;
222             }
223 
224             $this->controller = $this->err_controller;
225             $this->method = 'error';
226             $this->method_args = [404];
227             $this->tryCall();
228          } else {
229             throw new CE('No route available and the error controller '
230                . 'is invalid.' . ($msg ? ' Exception message returned: '
231                   . $msg : ''), CE::E_INVALID_ROUTE);
232          }
233 
234          return $this;
235       }
236 
237       /**
238        * Returns the error controller name
239        *
240        * @author Art <a.molcanovas@gmail.com>
241        * @return string
242        */
243       function getErrController() {
244          return $this->err_controller;
245       }
246 
247       /**
248        * Tries to call the appropriate class' method
249        *
250        * @author Art <a.molcanovas@gmail.com>
251        * @return Router
252        * @uses   self::forceError()
253        */
254       protected function tryCall() {
255          $rc = $rm = $init = false;
256 
257          try {
258             $rc = new ReflectionClass(self::CONTROLLER_NAMESPACE . $this->controller);
259 
260             //Must be abstract controller's subclass
261             if (!$rc->isAbstract() &&
262                $rc->isSubclassOf('\Alo\Controller\AbstractController')
263             ) {
264                $rm = $rc->getMethod($this->method);
265 
266                //And a public method
267                if ($rm->isPublic() && !$rm->isAbstract() && !$rm->isStatic()) {
268                   //Excellent. Instantiate!
269                   $init = true;
270                } else {
271                   $this->forceError();
272                }
273             } else {
274                $this->forceError();
275             }
276          } catch (\ReflectionException $ex) {
277             $this->forceError($ex->getMessage());
278          }
279 
280          if ($init) {
281             \Log::debug('Initialising controller ' . $this->controller . '->' . $this->method . '(' . implode(',', $this->method_args) . ')');
282             $controller_name = self::CONTROLLER_NAMESPACE . $this->controller;
283             Alo::$controller = new $controller_name;
284             call_user_func_array([Alo::$controller, $this->method], $this->method_args);
285          }
286 
287          return $this;
288       }
289 
290       /**
291        * Resolves the controller/method path
292        *
293        * @author Art <a.molcanovas@gmail.com>
294        * @return Router
295        * @todo   Remove comment end debug output
296        */
297       protected function resolvePath() {
298          //Use the default controller if the path is unavailable
299          if (!$this->path) {
300             $filepath = DIR_CONTROLLERS . strtolower($this->default_controller) . '.php';
301 
302             if (file_exists($filepath)) {
303                include_once $filepath;
304             }
305 
306             $this->controller = $this->default_controller;
307             $this->method = self::$route_defaults['method'];
308             $this->method_args = self::$route_defaults['args'];
309             $this->dir = self::$route_defaults['dir'];
310          } else {
311             $resolved = false;
312 
313             //Check if there's a route
314             foreach ($this->routes as $source => $dest) {
315                $source_replace = trim(str_replace(self::PREG_DELIMITER, '\\' . self::PREG_DELIMITER, $source), '/');
316                $regex = self::PREG_DELIMITER . '^' . $source_replace . '/?' . '$' . self::PREG_DELIMITER . 'is';
317 
318                if (preg_match($regex, $this->path)) {
319                   $resolved = true;
320                   $explode = explode('/', $this->path);
321 
322                   $this->dir = $dest['dir'] ? $dest['dir'] . DIRECTORY_SEPARATOR : self::$route_defaults['dir'];
323                   $this->controller = isset($dest['class']) ? $dest['class'] : $explode[0];
324 
325                   //Remove controller
326                   array_shift($explode);
327 
328                   //Set method
329                   if ($dest['method'] != self::$route_defaults['method']) {
330                      $this->method = $dest['method'];
331                   } elseif (isset($explode[0])) {
332                      $this->method = $explode[0];
333                   } else {
334                      $this->method = self::$route_defaults['method'];
335                   }
336 
337                   //Remove controller method
338                   if (!empty($explode)) {
339                      array_shift($explode);
340                   }
341 
342                   //Set preliminary method args
343                   if ($dest['args'] != self::$route_defaults['args']) {
344                      $this->method_args = $dest['args'];
345                   } elseif (!empty($explode)) {
346                      $this->method_args = $explode;
347                   } else {
348                      $this->method_args = self::$route_defaults['args'];
349                   }
350 
351                   //echo debug([
352                   //   'source_replace'    => $source_replace,
353                   //   'path'              => $this->path,
354                   //   'dest'              => $dest,
355                   //   '$this->dir'        => $this->dir,
356                   //   '$this->controller' => $this->controller,
357                   //   '$this->method'     => $this->method,
358                   //   '$this->args'       => $this->method_args,
359                   //   'regex'             => $regex,
360                   //   'replace_with'      => '[\'' . implode('\',\'', $this->method_args) . '\']',
361                   //   'replace_final'     => json_decode(preg_replace($regex, '["' . implode('","', $this->method_args) . '"]', $this->path), true)
362                   //]);
363 
364                   $replace = explode('/', preg_replace($regex, implode('/', $this->method_args), $this->path));
365 
366                   //Remove empties
367                   foreach ($replace as $k => $v) {
368                      if ($v == '') {
369                         unset($replace[$k]);
370                      }
371                   }
372 
373                   $this->method_args = $replace;
374 
375                   //echo debug($this->method_args);
376 
377                   break;
378                }
379             }
380 
381             if (!$resolved) {
382                //If not, assume the path is controller/method/arg1...
383                $path = explode('/', $this->path);
384 
385                $this->dir = null;
386                $this->controller = array_shift($path);
387                $this->method = empty($path) ? self::$route_defaults['method'] : array_shift($path);
388                $this->method_args = $path;
389             }
390 
391             $filepath = DIR_CONTROLLERS . str_replace('/', DIRECTORY_SEPARATOR, $this->dir) . $this->controller . '.php';
392 
393             if (file_exists($filepath)) {
394                include_once $filepath;
395             }
396          }
397 
398          return $this;
399       }
400 
401       /**
402        * Initialises the routing variables
403        *
404        * @author Art <a.molcanovas@gmail.com>
405        * @throws CE When the config file is not found
406        * @throws CE When $error_controller_class is not present in the config file
407        * @throws CE When The default controller is not present in the config file
408        * @throws CE When $routes is not a valid array
409        * @throws CE When a route value is not an array.
410        * @return Router
411        */
412       protected function init_routes() {
413          $path = \Alo::loadConfig('router', true);
414 
415          if (!file_exists($path)) {
416             throw new CE('Routing config file not found.', CE::E_CONFIG_NOT_FOUND);
417          } else {
418             require $path;
419 
420             if (!isset($error_controller_class)) {
421                throw new CE('Error controller class not found in config file.', CE::E_ERR_NOT_FOUND);
422             } elseif (!isset($default_controller)) {
423                throw new CE('$default_controller undefined in config file.', CE::E_DEFAULT_UNDEFINED);
424             } elseif (!is_array($routes)) {
425                throw new CE('The routes variable must be an associative array', CE::E_MALFORMED_ROUTES);
426             } else {
427                $this->err_controller = $error_controller_class;
428                $this->default_controller = $default_controller;
429 
430                foreach ($routes as $k => $v) {
431                   if (is_array($v)) {
432                      $this->routes[strtolower($k)] = array_merge(self::$route_defaults, $v);
433                   } else {
434                      throw new CE('Route ' . $k . ' is not a valid array.', CE::E_MALFORMED_ROUTES);
435                   }
436                }
437 
438                \Log::debug('Routes initialised');
439             }
440          }
441 
442          return $this;
443       }
444 
445       /**
446        * Initialises the raw path variable
447        *
448        * @author Art <a.molcanovas@gmail.com>
449        * @return Router
450        */
451       protected function init_path() {
452          if (isset($_SERVER['PATH_INFO'])) {
453             $this->path = ltrim($_SERVER['PATH_INFO'], '/');
454          } elseif (isset($_SERVER['argv'])) {
455             //Shift off the "index.php" bit
456             array_shift($_SERVER['argv']);
457             $this->path = join(DIRECTORY_SEPARATOR, $_SERVER['argv']);
458          } else {
459             $this->path = '';
460          }
461 
462          $this->path = strtolower($this->path);
463 
464          return $this;
465       }
466 
467       /**
468        * Initialises most server variables
469        *
470        * @author Art <a.molcanovas@gmail.com>
471        * @return Router
472        */
473       protected function init_server_vars() {
474          $this->port = \get($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : null;
475          $this->remote_addr = \get($_SERVER['REMOTE_ADDR']);
476          $this->request_scheme = \get($_SERVER['REQUEST_SCHEME']);
477          $this->request_method = \get($_SERVER['REQUEST_METHOD']);
478          $this->server_addr = \get($_SERVER['SERVER_ADDR']);
479          $this->server_name = \get($_SERVER['SERVER_NAME']);
480 
481          return $this;
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->remote_addr;
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->request_method;
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->request_scheme;
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->server_addr;
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->server_name;
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(\lite_debug($this));
592       }
593 
594    }
AloFramework documentation API documentation generated by ApiGen 2.8.0