RouteCollection.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Routing;
  16. use Cake\Routing\Exception\MissingRouteException;
  17. use Cake\Routing\Route\Route;
  18. /**
  19. * Contains a collection of routes.
  20. *
  21. * Provides an interface for adding/removing routes
  22. * and parsing/generating URLs with the routes it contains.
  23. *
  24. * @internal
  25. */
  26. class RouteCollection
  27. {
  28. /**
  29. * The routes connected to this collection.
  30. *
  31. * @var array
  32. */
  33. protected $_routeTable = [];
  34. /**
  35. * The routes connected to this collection.
  36. *
  37. * @var array
  38. */
  39. protected $_routes = [];
  40. /**
  41. * The hash map of named routes that are in this collection.
  42. *
  43. * @var array
  44. */
  45. protected $_named = [];
  46. /**
  47. * Routes indexed by path prefix.
  48. *
  49. * @var array
  50. */
  51. protected $_paths = [];
  52. /**
  53. * Route extensions
  54. *
  55. * @var array
  56. */
  57. protected $_extensions = [];
  58. /**
  59. * Add a route to the collection.
  60. *
  61. * @param \Cake\Routing\Route\Route $route The route object to add.
  62. * @param array $options Additional options for the route. Primarily for the
  63. * `_name` option, which enables named routes.
  64. * @return void
  65. */
  66. public function add(Route $route, array $options = [])
  67. {
  68. $this->_routes[] = $route;
  69. // Explicit names
  70. if (isset($options['_name'])) {
  71. $this->_named[$options['_name']] = $route;
  72. }
  73. // Generated names.
  74. $name = $route->getName();
  75. if (!isset($this->_routeTable[$name])) {
  76. $this->_routeTable[$name] = [];
  77. }
  78. $this->_routeTable[$name][] = $route;
  79. // Index path prefixes (for parsing)
  80. $path = $route->staticPath();
  81. if (empty($this->_paths[$path])) {
  82. $this->_paths[$path] = [];
  83. krsort($this->_paths);
  84. }
  85. $this->_paths[$path][] = $route;
  86. $extensions = $route->extensions();
  87. if ($extensions) {
  88. $this->extensions($extensions);
  89. }
  90. }
  91. /**
  92. * Takes the URL string and iterates the routes until one is able to parse the route.
  93. *
  94. * @param string $url URL to parse.
  95. * @param string $method The HTTP method to use.
  96. * @return array An array of request parameters parsed from the URL.
  97. * @throws \Cake\Routing\Exception\MissingRouteException When a URL has no matching route.
  98. */
  99. public function parse($url, $method = '')
  100. {
  101. $decoded = urldecode($url);
  102. foreach (array_keys($this->_paths) as $path) {
  103. if (strpos($decoded, $path) !== 0) {
  104. continue;
  105. }
  106. $queryParameters = null;
  107. if (strpos($url, '?') !== false) {
  108. list($url, $queryParameters) = explode('?', $url, 2);
  109. parse_str($queryParameters, $queryParameters);
  110. }
  111. foreach ($this->_paths[$path] as $route) {
  112. $r = $route->parse($url, $method);
  113. if ($r === false) {
  114. continue;
  115. }
  116. if ($queryParameters) {
  117. $r['?'] = $queryParameters;
  118. }
  119. return $r;
  120. }
  121. }
  122. throw new MissingRouteException(['url' => $url]);
  123. }
  124. /**
  125. * Get the set of names from the $url. Accepts both older style array urls,
  126. * and newer style urls containing '_name'
  127. *
  128. * @param array $url The url to match.
  129. * @return array The set of names of the url
  130. */
  131. protected function _getNames($url)
  132. {
  133. $plugin = false;
  134. if (isset($url['plugin']) && $url['plugin'] !== false) {
  135. $plugin = strtolower($url['plugin']);
  136. }
  137. $prefix = false;
  138. if (isset($url['prefix']) && $url['prefix'] !== false) {
  139. $prefix = strtolower($url['prefix']);
  140. }
  141. $controller = strtolower($url['controller']);
  142. $action = strtolower($url['action']);
  143. $names = [
  144. "${controller}:${action}",
  145. "${controller}:_action",
  146. "_controller:${action}",
  147. "_controller:_action"
  148. ];
  149. // No prefix, no plugin
  150. if ($prefix === false && $plugin === false) {
  151. return $names;
  152. }
  153. // Only a plugin
  154. if ($prefix === false) {
  155. return [
  156. "${plugin}.${controller}:${action}",
  157. "${plugin}.${controller}:_action",
  158. "${plugin}._controller:${action}",
  159. "${plugin}._controller:_action",
  160. "_plugin.${controller}:${action}",
  161. "_plugin.${controller}:_action",
  162. "_plugin._controller:${action}",
  163. "_plugin._controller:_action",
  164. ];
  165. }
  166. // Only a prefix
  167. if ($plugin === false) {
  168. return [
  169. "${prefix}:${controller}:${action}",
  170. "${prefix}:${controller}:_action",
  171. "${prefix}:_controller:${action}",
  172. "${prefix}:_controller:_action",
  173. "_prefix:${controller}:${action}",
  174. "_prefix:${controller}:_action",
  175. "_prefix:_controller:${action}",
  176. "_prefix:_controller:_action"
  177. ];
  178. }
  179. // Prefix and plugin has the most options
  180. // as there are 4 factors.
  181. return [
  182. "${prefix}:${plugin}.${controller}:${action}",
  183. "${prefix}:${plugin}.${controller}:_action",
  184. "${prefix}:${plugin}._controller:${action}",
  185. "${prefix}:${plugin}._controller:_action",
  186. "${prefix}:_plugin.${controller}:${action}",
  187. "${prefix}:_plugin.${controller}:_action",
  188. "${prefix}:_plugin._controller:${action}",
  189. "${prefix}:_plugin._controller:_action",
  190. "_prefix:${plugin}.${controller}:${action}",
  191. "_prefix:${plugin}.${controller}:_action",
  192. "_prefix:${plugin}._controller:${action}",
  193. "_prefix:${plugin}._controller:_action",
  194. "_prefix:_plugin.${controller}:${action}",
  195. "_prefix:_plugin.${controller}:_action",
  196. "_prefix:_plugin._controller:${action}",
  197. "_prefix:_plugin._controller:_action",
  198. ];
  199. }
  200. /**
  201. * Reverse route or match a $url array with the defined routes.
  202. * Returns either the string URL generate by the route, or false on failure.
  203. *
  204. * @param array $url The url to match.
  205. * @param array $context The request context to use. Contains _base, _port,
  206. * _host, _scheme and params keys.
  207. * @return string|false Either a string on match, or false on failure.
  208. * @throws \Cake\Routing\Exception\MissingRouteException when a route cannot be matched.
  209. */
  210. public function match($url, $context)
  211. {
  212. // Named routes support optimization.
  213. if (isset($url['_name'])) {
  214. $name = $url['_name'];
  215. unset($url['_name']);
  216. $out = false;
  217. if (isset($this->_named[$name])) {
  218. $route = $this->_named[$name];
  219. $out = $route->match($url + $route->defaults, $context);
  220. }
  221. if ($out) {
  222. return $out;
  223. }
  224. throw new MissingRouteException(['url' => $name, 'context' => $context]);
  225. }
  226. foreach ($this->_getNames($url) as $name) {
  227. if (empty($this->_routeTable[$name])) {
  228. continue;
  229. }
  230. foreach ($this->_routeTable[$name] as $route) {
  231. $match = $route->match($url, $context);
  232. if ($match) {
  233. return strlen($match) > 1 ? trim($match, '/') : $match;
  234. }
  235. }
  236. }
  237. throw new MissingRouteException(['url' => var_export($url, true), 'context' => $context]);
  238. }
  239. /**
  240. * Get all the connected routes as a flat list.
  241. *
  242. * @return array
  243. */
  244. public function routes()
  245. {
  246. return $this->_routes;
  247. }
  248. /**
  249. * Get the connected named routes.
  250. *
  251. * @return array
  252. */
  253. public function named()
  254. {
  255. return $this->_named;
  256. }
  257. /**
  258. * Get/set the extensions that the route collection could handle.
  259. *
  260. * @param null|string|array $extensions Either the list of extensions to set,
  261. * or null to get.
  262. * @param bool $merge Whether to merge with or override existing extensions.
  263. * Defaults to `true`.
  264. * @return array The valid extensions.
  265. */
  266. public function extensions($extensions = null, $merge = true)
  267. {
  268. if ($extensions === null) {
  269. return $this->_extensions;
  270. }
  271. $extensions = (array)$extensions;
  272. if ($merge) {
  273. $extensions = array_unique(array_merge(
  274. $this->_extensions,
  275. $extensions
  276. ));
  277. }
  278. return $this->_extensions = $extensions;
  279. }
  280. }