PageRenderTime 44ms CodeModel.GetById 9ms RepoModel.GetById 1ms app.codeStats 0ms

/aurora/routing/router.php

https://gitlab.com/Bartwillemsen/aurora-framework
PHP | 275 lines | 104 code | 36 blank | 135 comment | 13 complexity | c4e4522a3cbbd912b23b6ee87501b512 MD5 | raw file
  1. <?php
  2. namespace Aurora\Routing;
  3. use Aurora\Request;
  4. class Delegate
  5. {
  6. /**
  7. * The destination of the route delegate.
  8. *
  9. * @var string
  10. */
  11. public $destination;
  12. /**
  13. * Create a new route delegate instance.
  14. *
  15. * @param string $destination
  16. */
  17. public function __construct($destination)
  18. {
  19. $this->destination = $destination;
  20. }
  21. }
  22. class Router
  23. {
  24. /**
  25. * The route loader instance.
  26. *
  27. * @var Loader
  28. */
  29. public $loader;
  30. /**
  31. * The named routes that have been found so far.
  32. *
  33. * @var array
  34. */
  35. protected $names = array();
  36. /**
  37. * The path the application controllers.
  38. *
  39. * @var string
  40. */
  41. protected $controllers;
  42. /**
  43. * The wildcard patterns supported by the router.
  44. *
  45. * @var array
  46. */
  47. protected $patterns = array(
  48. '(:num)' => '([0-9]+)',
  49. '(:any)' => '([a-zA-Z0-9\.\-_]+)',
  50. );
  51. /**
  52. * The optional wildcard patterns supported by the router.
  53. *
  54. * @var array
  55. */
  56. protected $optional = array(
  57. '/(:num?)' => '(?:/([0-9]+)',
  58. '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_]+)',
  59. );
  60. /**
  61. * Create a new router for a request method and URI.
  62. *
  63. * @param Loader $loader
  64. * @param string $controllers
  65. */
  66. public function __construct(Loader $loader, $controllers)
  67. {
  68. $this->loader = $loader;
  69. $this->controllers = $controllers;
  70. }
  71. /**
  72. * Find a route by name.
  73. *
  74. * The returned array will be identical the array defined in the routes.php file.
  75. *
  76. * @param string $name
  77. * @return array
  78. */
  79. public function find($name)
  80. {
  81. if (array_key_exists($name, $this->names)) return $this->names[$name];
  82. // To find a named route, we need to iterate through every route defined
  83. // for the application. We will cache the routes by name so we can load
  84. // them very quickly if we need to find them a second time.
  85. foreach ($this->loader->everything() as $key => $value) {
  86. if (is_array($value) and isset($value['name']) and $value['name'] == $name) {
  87. return $this->names[$name] = array($key => $value);
  88. }
  89. }
  90. }
  91. /**
  92. * Search the routes for the route matching a method and URI.
  93. *
  94. * @param string $method
  95. * @param string $uri
  96. * @return Route
  97. */
  98. public function route($method, $uri)
  99. {
  100. $routes = $this->loader->load($uri);
  101. // All route URIs begin with the request method and have a leading
  102. // slash before the URI. We'll put the request method and URI into
  103. // that format so we can easily check for literal matches.
  104. $destination = $method.' /'.trim($uri, '/');
  105. if (isset($routes[$destination])) {
  106. return new Route($destination, $routes[$destination], array());
  107. }
  108. // If no literal route match was found, we will iterate through all
  109. // of the routes and check each of them one at a time, translating
  110. // any wildcards in the route into actual regular expressions.
  111. foreach ($routes as $keys => $callback) {
  112. // Only check the routes that couldn't be matched literally...
  113. if (strpos($keys, '(') !== false or strpos($keys, ',') !== false) {
  114. if (! is_null($route = $this->match($destination, $keys, $callback))) {
  115. return $route;
  116. }
  117. }
  118. }
  119. return $this->controller($method, $uri, $destination);
  120. }
  121. /**
  122. * Attempt to match a given route destination to a given route.
  123. *
  124. * The destination's method and URIs will be compared against the route's.
  125. * If there is a match, the Route instance will be returned, otherwise null
  126. * will be returned by the method.
  127. *
  128. * @param string $destination
  129. * @param array $keys
  130. * @param mixed $callback
  131. * @return mixed
  132. */
  133. protected function match($destination, $keys, $callback)
  134. {
  135. foreach (explode(', ', $keys) as $key) {
  136. if (preg_match('#^'.$this->wildcards($key).'$#', $destination)) {
  137. return new Route($keys, $callback, $this->parameters($destination, $key));
  138. }
  139. }
  140. }
  141. /**
  142. * Attempt to find a controller for the incoming request.
  143. *
  144. * @param string $method
  145. * @param string $uri
  146. * @param string $destination
  147. * @return Route
  148. */
  149. protected function controller($method, $uri, $destination)
  150. {
  151. // If the request is to the root of the application, an ad-hoc route
  152. // will be generated to the home controller's "index" method, making
  153. // it the default controller method.
  154. if ($uri === '/') return new Route($method.' /', 'home@index');
  155. $segments = explode('/', trim($uri, '/'));
  156. // If there are more than 20 request segments, we will halt the request
  157. // and throw an exception. This is primarily to protect against DDoS
  158. // attacks which could overwhelm the server by feeding it too many
  159. // segments in the URI, causing the loops in this class to bog.
  160. if (count($segments) > 20) {
  161. throw new \Exception("Invalid request. There are more than 20 URI segments.");
  162. }
  163. if (! is_null($key = $this->controller_key($segments))) {
  164. // Extract the various parts of the controller call from the URI.
  165. // First, we'll extract the controller name, then, since we need
  166. // to extract the method and parameters, we will remove the name
  167. // of the controller from the URI. Then we can shift the method
  168. // off of the array of segments. Any remaining segments are the
  169. // parameters that should be passed to the controller method.
  170. $controller = implode('.', array_slice($segments, 0, $key));
  171. $segments = array_slice($segments, $key);
  172. $method = (count($segments) > 0) ? array_shift($segments) : 'index';
  173. return new Route($destination, $controller.'@'.$method, $segments);
  174. }
  175. }
  176. /**
  177. * Search the controllers that can handle the current request.
  178. *
  179. * If a controller is found, the array key for the controller name in the URI
  180. * segments will be returned by the method, otherwise NULL will be returned.
  181. * The deepest possible controller will be considered the controller that
  182. * should handle the request.
  183. *
  184. * @param array $segments
  185. * @return int
  186. */
  187. protected function controller_key($segments)
  188. {
  189. // To find the proper controller, we need to iterate backwards through
  190. // the URI segments and take the first file that matches. That file
  191. // should be the deepest controller matched by the URI.
  192. foreach (array_reverse($segments, true) as $key => $value) {
  193. $controller = implode('/', array_slice($segments, 0, $key + 1)).EXT;
  194. if (file_exists($path = $this->controllers.$controller)) {
  195. return $key + 1;
  196. }
  197. }
  198. }
  199. /**
  200. * Translate route URI wildcards into actual regular expressions.
  201. *
  202. * @param string $key
  203. * @return string
  204. */
  205. private function wildcards($key)
  206. {
  207. // For optional parameters, first translate the wildcards to their
  208. // regex equivalent, sans the ")?" ending. We will add the endings
  209. // back on after we know how many replacements we made.
  210. $key = str_replace(array_keys($this->optional), array_values($this->optional), $key, $count);
  211. $key .= ($count > 0) ? str_repeat(')?', $count) : '';
  212. return str_replace(array_keys($this->patterns), array_values($this->patterns), $key);
  213. }
  214. /**
  215. * Extract the parameters from a URI based on a route URI.
  216. *
  217. * Any route segment wrapped in parentheses is considered a parameter.
  218. *
  219. * @param string $uri
  220. * @param string $route
  221. * @return array
  222. */
  223. private function parameters($uri, $route)
  224. {
  225. list($uri, $route) = array(explode('/', $uri), explode('/', $route));
  226. $count = count($route);
  227. $parameters = array();
  228. // To find the parameters that should be passed to the route, we will
  229. // iterate through the route segments, and if the segment is enclosed
  230. // in parentheses, we will take the matching segment from the request
  231. // URI and add it to the array of parameters.
  232. for ($i = 0; $i < $count; $i++) {
  233. if (preg_match('/\(.+\)/', $route[$i]) and isset($uri[$i])) {
  234. $parameters[] = $uri[$i];
  235. }
  236. }
  237. return $parameters;
  238. }
  239. }