1 <?php
2 /**
3 * Router.php
4 *
5 * @author Elvyrra SAS
6 * @license http://rem.mit-license.org/ MIT
7 */
8
9 namespace Hawk;
10
11 /**
12 * This class describes the application router. It is used in any plugin to route URIs to controllers methods
13 *
14 * @package Core\Router
15 */
16 final class Router extends Singleton{
17 /**
18 * Invalid URL. This URI is displayed when no URI was found for a given route name
19 */
20 const INVALID_URL = '/INVALID_URL';
21
22 /**
23 * The defined routes
24 *
25 * @var array
26 */
27 private $routes = array(),
28
29 /**
30 * The routes accessible for the current request method
31 */
32 $activeRoutes = array(),
33
34 /**
35 * The current route, associated to the current URI
36 */
37 $currentRoute,
38
39 /**
40 * The current controller instance, associated to the current uri
41 */
42 $currentController,
43
44 /**
45 * The authentications required to match the URIs
46 */
47 $auth = array(),
48
49
50 /**
51 * The predefined data for the routes
52 */
53 $predefinedData = array();
54
55
56 /**
57 * The router instance
58 */
59 protected static $instance;
60
61 /**
62 * Add a new accessible route to the router
63 *
64 * @param string $method The HTTP method the route is accessible for
65 * @param string $name The route name. This name must be unique for each route
66 * @param string $uri The route URI, defined like : /path/{param1}/to/{param2}
67 * @param array $param The route parameters. This array can have the following data :
68 * <ul>
69 * <li>'action' string (required) : The controller method to call when the route is matched,
70 * formatted like this : 'ControllerClass.method'
71 * </li>
72 * <li>'where' array (optionnal) : An array defining each parameter pattern,
73 * where keys are the names of the route parameters,
74 * and values are the regular expression to match (without delimiters).
75 * </li>
76 * <li>'default' array (optionnal) : An array defining the default values of parameters.
77 * This is useful to generate a URI from a route name (method getUri),
78 * without giving all parameters values
79 * </li>
80 * </ul>
81 */
82 private function add($method, $name, $uri, $param){
83 if(isset($param['auth'])) {
84 $auth = $param['auth'];
85 $param['auth'] = $this->auth;
86 $param['auth'][] = $auth;
87 }
88 else{
89 $param['auth'] = $this->auth;
90 }
91
92 foreach($this->predefinedData as $key => $value){
93 $param[$key] = $value;
94 }
95
96 if(!isset($param['namespace'])) {
97 $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 2);
98 $param['namespace'] = Plugin::getFilePlugin($trace[1]['file'])->getNamespace();
99 }
100
101
102 if(isset($this->routes[$name])) {
103 trigger_error("The route named '$name' already exists", E_USER_WARNING);
104 }
105 else{
106 $route = new Route($name, $uri, $param);
107
108 $this->routes[$name] = &$route;
109
110 if(App::request()->getMethod() == $method || $method == 'any') {
111 $this->activeRoutes[$name] = &$route;
112 }
113 }
114 }
115
116 /**
117 * Add an authentication condition to match the routes defined inside $action callback.
118 * For example, you can write something like :
119 *
120 * <code>
121 * App::router()->auth(App::session()->getUser()->isAllowed('admin.all'), function(){
122 * App::router()->get('test-route', '/test', array('action' => 'TestController.testMethod'));
123 * });
124 * </code>
125 *
126 * If the user tries to access to /test without the necessary privileges,
127 * then a HTTP code 403 (Forbidden) will be returned
128 *
129 * @param boolean $auth The authentication. If true, then the routes inside are accessible, else they're not
130 * @param callable $action The function that defines the routes under this authentication
131 */
132 public function auth($auth, $action){
133 // Add the authentication for all following route
134 $this->auth[] = $auth;
135
136 // Execute the action
137 $action();
138
139 // Remove the authentication for the rest of the scripts
140 array_pop($this->auth);
141 }
142
143
144 /**
145 * Set properties for all the routes that are defined in the $action callback.
146 * It can be used to set a prefix to a set of routes, a namespace for all routes actions, ...
147 *
148 * @param array $data The properties to set
149 * @param callable $action The function that defines the routes with these properties
150 */
151 public function setProperties($data, $action){
152 $currentData = $this->predefinedData;
153 foreach($data as $key => $value){
154 $this->predefinedData[$key] = $value;
155 }
156
157 $action();
158
159 $this->predefinedData = $currentData;
160 }
161
162
163 /**
164 * Set a prefix to routes URIs that are defined in $action callback
165 *
166 * @param string $prefix The prefix to set to the URIs
167 * @param callable $action The function that defined the routes with this prefix
168 */
169 public function prefix($prefix, $action){
170 $this->setProperties(array('prefix' => $prefix), $action);
171 }
172
173
174 /**
175 * Add a route acessible by GET HTTP requests
176 *
177 * @param string $name The route name. This name must be unique for each route
178 * @param string $path The route path, defined like : /path/{param1}/to/{param2}
179 * @param array $param The route parameters. This array can have the following data :
180 * <ul>
181 * <li>'action' string (required) : The controller method to call when the route is matched,
182 * formatted like this : 'ControllerClass.method'
183 * </li>
184 * <li>'where' array (optionnal) : An array defining each parameter pattern,
185 * where keys are the names of the route parameters,
186 * and values are the regular expression to match (without delimiters).
187 * </li>
188 * <li>'default' array (optionnal) : An array defining the default values of parameters.
189 * This is useful to generate a URI from a route name (method getUri),
190 * without giving all parameters values
191 * </li>
192 * </ul>
193 */
194 public function get($name, $path, $param){
195 $this->add('get', $name, $path, $param);
196 }
197
198
199 /**
200 * Add a route acessible by POST HTTP requests
201 *
202 * @param string $name The route name. This name must be unique for each route
203 * @param string $path The route path, defined like : /path/{param1}/to/{param2}
204 * @param array $param The route parameters. This array can have the following data :
205 * <ul>
206 * <li>'action' string (required) : The controller method to call when the route is matched,
207 * formatted like this : 'ControllerClass.method'
208 * </li>
209 * <li>'where' array (optionnal) : An array defining each parameter pattern,
210 * where keys are the names of the route parameters,
211 * and values are the regular expression to match (without delimiters).
212 * </li>
213 * <li>'default' array (optionnal) : An array defining the default values of parameters.
214 * This is useful to generate a URI from a route name (method getUri),
215 * without giving all parameters values
216 * </li>
217 * </ul>
218 */
219 public function post($name, $path, $param){
220 $this->add('post', $name, $path, $param);
221 }
222
223
224 /**
225 * Add a route acessible by PUT HTTP requests
226 *
227 * @param string $name The route name. This name must be unique for each route
228 * @param string $path The route path, defined like : /path/{param1}/to/{param2}
229 * @param array $param The route parameters. This array can have the following data :
230 * <ul>
231 * <li>'action' string (required) : The controller method to call when the route is matched,
232 * formatted like this : 'ControllerClass.method'
233 * </li>
234 * <li>'where' array (optionnal) : An array defining each parameter pattern,
235 * where keys are the names of the route parameters,
236 * and values are the regular expression to match (without delimiters).
237 * </li>
238 * <li>'default' array (optionnal) : An array defining the default values of parameters.
239 * This is useful to generate a URI from a route name (method getUri),
240 * without giving all parameters values
241 * </li>
242 * </ul>
243 */
244 public function put($name, $path, $param){
245 $this->add('put', $name, $path, $param);
246 }
247
248
249 /**
250 * Add a route acessible by DELETE HTTP requests
251 *
252 * @param string $name The route name. This name must be unique for each route
253 * @param string $path The route path, defined like : /path/{param1}/to/{param2}
254 * @param array $param The route parameters. This array can have the following data :
255 * <ul>
256 * <li>'action' string (required) : The controller method to call when the route is matched,
257 * formatted like this : 'ControllerClass.method'
258 * </li>
259 * <li>'where' array (optionnal) : An array defining each parameter pattern,
260 * where keys are the names of the route parameters,
261 * and values are the regular expression to match (without delimiters).
262 * </li>
263 * <li>'default' array (optionnal) : An array defining the default values of parameters.
264 * This is useful to generate a URI from a route name (method getUri),
265 * without giving all parameters values
266 * </li>
267 * </ul>
268 */
269 public function delete($name, $path, $param){
270 $this->add('delete', $name, $path, $param);
271 }
272
273
274 /**
275 * Add a route acessible by PATCH HTTP requests
276 *
277 * @param string $name The route name. This name must be unique for each route
278 * @param string $path The route path, defined like : /path/{param1}/to/{param2}
279 * @param array $param The route parameters. This array can have the following data :
280 * <ul>
281 * <li>'action' string (required) : The controller method to call when the route is matched,
282 * formatted like this : 'ControllerClass.method'
283 * </li>
284 * <li>'where' array (optionnal) : An array defining each parameter pattern,
285 * where keys are the names of the route parameters,
286 * and values are the regular expression to match (without delimiters).
287 * </li>
288 * <li>'default' array (optionnal) : An array defining the default values of parameters.
289 * This is useful to generate a URI from a route name (method getUri),
290 * without giving all parameters values
291 * </li>
292 * </ul>
293 */
294 public function patch($name, $path, $param){
295 $this->add('patch', $name, $path, $param);
296 }
297
298 /**
299 * Add a route acessible by GET, POST OR DELETE HTTP requests
300 *
301 * @param string $name The route name. This name must be unique for each route
302 * @param string $path The route path, defined like : /path/{param1}/to/{param2}
303 * @param array $param The route parameters. This array can have the following data :
304 * <ul>
305 * <li>'action' string (required) : The controller method to call when the route is matched,
306 * formatted like this : 'ControllerClass.method'
307 * </li>
308 * <li>'where' array (optionnal) : An array defining each parameter pattern,
309 * where keys are the names of the route parameters,
310 * and values are the regular expression to match (without delimiters).
311 * </li>
312 * <li>'default' array (optionnal) : An array defining the default values of parameters.
313 * This is useful to generate a URI from a route name (method getUri),
314 * without giving all parameters values
315 * </li>
316 * </ul>
317 */
318 public function any($name, $path, $param){
319 $this->add('any', $name, $path, $param);
320 }
321
322
323 /**
324 * Compute the routing, and execute the controller method associated to the URI
325 */
326 public function route(){
327 $path = str_replace(BASE_PATH, '', parse_url(App::request()->getUri(), PHP_URL_PATH));
328
329 // Scan each row
330 foreach($this->activeRoutes as $route){
331 if($route->match($path)) {
332 // The URI matches with the route
333 $this->currentRoute = &$route;
334
335 // Emit an event, saying the routing action is finished
336 $event = new Event(
337 'after-routing', array(
338 'route' => $route,
339 )
340 );
341 $event->trigger();
342
343 $route = $event->getData('route');
344
345 if($route->isAccessible()) {
346 // The route authentications are validated
347
348
349 list($classname, $method) = explode(".", $route->action);
350
351 // call a controller method
352 $this->currentController = new $classname($route->getData());
353 App::logger()->debug(sprintf(
354 'URI %s has been routed => %s::%s',
355 App::request()->getUri(),
356 $classname,
357 $method
358 ));
359
360 // Set the controller result to the HTTP response
361 App::response()->setBody($this->currentController->compute($method));
362 }
363 else{
364
365 // The route is not accessible
366 App::logger()->warning(sprintf(
367 'A user with the IP address %s tried to access %s without the necessary privileges',
368 App::request()->clientIp(),
369 App::request()->getUri()
370 ));
371
372 App::response()->setStatus(403);
373 $response = array(
374 'message' => Lang::get('main.403-message'),
375 'reason' => !App::session()->isLogged() ? 'login' : 'permission'
376 );
377
378 if(App::request()->isAjax()) {
379 App::response()->setContentType('json');
380 App::response()->setBody($response);
381 }
382 else{
383 App::response()->setBody($response['message']);
384 }
385
386 throw new AppStopException();
387 }
388 return;
389 }
390 }
391
392 // The route was not found
393 App::logger()->warning('The URI ' . App::request()->getUri() . ' has not been routed');
394 App::response()->setStatus(404);
395 App::response()->setBody(Lang::get('main.404-message', array('uri' => $path)));
396 }
397
398
399 /**
400 * Get all defined routes
401 *
402 * @return array The defined routes
403 */
404 public function getRoutes(){
405 return $this->routes;
406 }
407
408 /**
409 * Get the routes accessible for the current HTTP request method
410 *
411 * @return array The list of the accessible routes
412 */
413 public function getActiveRoutes(){
414 return $this->activeRoutes;
415 }
416
417
418 /**
419 * Get the route corresponding to the current HTTP request
420 *
421 * @return Route The current route
422 */
423 public function getCurrentRoute(){
424 return isset($this->currentRoute) ? $this->currentRoute : null;
425 }
426
427 /**
428 * Get the action parameter of the current route
429 *
430 * @return string The action of the current route
431 */
432 public function getCurrentAction(){
433 return isset($this->currentRoute) ? $this->currentRoute->action : '';
434 }
435
436 /**
437 * Get the last instanciated controller
438 *
439 * @return Controller The last instanciated controller
440 */
441 public function getCurrentController(){
442 return $this->currentController;
443 }
444
445 /**
446 * Generate an URI from a given controller method (or route name) and its arguments.
447 *
448 * @param string $name The route name of the controller method, formatted like this : 'ControllerClass.method'
449 * @param array $args The route arguments, where keys define the parameters names and values, the values to affect.
450 *
451 * @return string The generated URI, or the current URI (if $method is not set)
452 */
453 public function getUri($name, $args= array()){
454
455 $route = $this->getRouteByAction($name);
456
457 if(empty($route)) {
458 return self::INVALID_URL;
459 }
460
461 $url = $route->url;
462 foreach($route->args as $arg){
463 if(isset($args[$arg])) {
464 $replace = $args[$arg];
465 }
466 elseif(isset($route->default[$arg])) {
467 $replace = $route->default[$arg];
468 }
469 else{
470 throw new \Exception("The URI built from '$method' needs the argument : $arg");
471 }
472 $url = str_replace("{{$arg}}", $replace, $url);
473 }
474
475 return BASE_PATH . $url;
476 }
477
478
479 /**
480 * Generate a full URL from a given controller method (or route name) and its arguments.
481 *
482 * @param string $name The route name of the controller method, formatted like this : 'ControllerClass.method'
483 * @param array $args The route arguments, where keys define the parameters names and values, the values to affect.
484 *
485 * @return string The generated URI, or the current URI (if $method is not set)
486 */
487 public function getUrl($name = '', $args = array()){
488 return ROOT_URL . $this->getUri($name, $args);
489 }
490
491
492 /**
493 * Get a route by action
494 *
495 * @param string $name The route name of the controller method, formatted like this : 'ControllerClass.method'
496 *
497 * @return Route The route corresponding to research
498 */
499 public function getRouteByAction($name){
500 $route = null;
501 if(isset($this->routes[$name])) {
502 return $this->routes[$name];
503 }
504
505 return null;
506 }
507
508
509 /**
510 * Find a route from an URI
511 *
512 * @param string $path The path to search the associated route
513 *
514 * @return Route the found route
515 */
516 public function getRouteByUri($path){
517 foreach($this->routes as $route){
518 if($route->match($path)) {
519 return $route;
520 }
521 }
522
523 return null;
524 }
525
526
527 /**
528 * Get a route by it name
529 *
530 * @param string $name The route name
531 *
532 * @return Route The found route
533 */
534 public function getRouteByName($name){
535 return isset($this->routes[$name]) ? $this->routes[$name] : null;
536 }
537
538 }