RouteCollection.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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. * The routes connected to this collection.
  29. *
  30. * @var array
  31. */
  32. protected $_routeTable = [];
  33. /**
  34. * The routes connected to this collection.
  35. *
  36. * @var array
  37. */
  38. protected $_routes = [];
  39. /**
  40. * The hash map of named routes that are in this collection.
  41. *
  42. * @var array
  43. */
  44. protected $_named = [];
  45. /**
  46. * Routes indexed by path prefix.
  47. *
  48. * @var array
  49. */
  50. protected $_paths = [];
  51. /**
  52. * Route extensions
  53. *
  54. * @var array
  55. */
  56. protected $_extensions = [];
  57. /**
  58. * Add a route to the collection.
  59. *
  60. * @param \Cake\Routing\Route\Route $route The route object to add.
  61. * @param array $options Additional options for the route. Primarily for the
  62. * `_name` option, which enables named routes.
  63. * @return void
  64. */
  65. public function add(Route $route, array $options = []) {
  66. $this->_routes[] = $route;
  67. // Explicit names
  68. if (isset($options['_name'])) {
  69. $this->_named[$options['_name']] = $route;
  70. }
  71. // Generated names.
  72. $name = $route->getName();
  73. if (!isset($this->_routeTable[$name])) {
  74. $this->_routeTable[$name] = [];
  75. }
  76. $this->_routeTable[$name][] = $route;
  77. // Index path prefixes (for parsing)
  78. $path = $route->staticPath();
  79. if (empty($this->_paths[$path])) {
  80. $this->_paths[$path] = [];
  81. krsort($this->_paths);
  82. }
  83. $this->_paths[$path][] = $route;
  84. $extensions = $route->extensions();
  85. if ($extensions) {
  86. $this->extensions($extensions);
  87. }
  88. }
  89. /**
  90. * Takes the URL string and iterates the routes until one is able to parse the route.
  91. *
  92. * @param string $url URL to parse.
  93. * @return array An array of request parameters parsed from the URL.
  94. * @throws \Cake\Routing\Exception\MissingRouteException When a URL has no matching route.
  95. */
  96. public function parse($url) {
  97. foreach (array_keys($this->_paths) as $path) {
  98. if (strpos($url, $path) !== 0) {
  99. continue;
  100. }
  101. $queryParameters = null;
  102. if (strpos($url, '?') !== false) {
  103. list($url, $queryParameters) = explode('?', $url, 2);
  104. parse_str($queryParameters, $queryParameters);
  105. }
  106. foreach ($this->_paths[$path] as $route) {
  107. $r = $route->parse($url);
  108. if ($r === false) {
  109. continue;
  110. }
  111. if ($queryParameters) {
  112. $r['?'] = $queryParameters;
  113. }
  114. return $r;
  115. }
  116. }
  117. throw new MissingRouteException(['url' => $url]);
  118. }
  119. /**
  120. * Get the set of names from the $url. Accepts both older style array urls,
  121. * and newer style urls containing '_name'
  122. *
  123. * @param array $url The url to match.
  124. * @return array The set of names of the url
  125. */
  126. protected function _getNames($url) {
  127. $plugin = false;
  128. if (isset($url['plugin']) && $url['plugin'] !== false) {
  129. $plugin = strtolower($url['plugin']);
  130. }
  131. $prefix = false;
  132. if (isset($url['prefix']) && $url['prefix'] !== false) {
  133. $prefix = strtolower($url['prefix']);
  134. }
  135. $controller = strtolower($url['controller']);
  136. $action = strtolower($url['action']);
  137. $names = [
  138. "${controller}:${action}",
  139. "${controller}:_action",
  140. "_controller:${action}",
  141. "_controller:_action"
  142. ];
  143. // No prefix, no plugin
  144. if ($prefix === false && $plugin === false) {
  145. return $names;
  146. }
  147. // Only a plugin
  148. if ($prefix === false) {
  149. return [
  150. "${plugin}.${controller}:${action}",
  151. "${plugin}.${controller}:_action",
  152. "${plugin}._controller:${action}",
  153. "${plugin}._controller:_action",
  154. "_plugin.${controller}:${action}",
  155. "_plugin.${controller}:_action",
  156. "_plugin._controller:${action}",
  157. "_plugin._controller:_action",
  158. ];
  159. }
  160. // Only a prefix
  161. if ($plugin === false) {
  162. return [
  163. "${prefix}:${controller}:${action}",
  164. "${prefix}:${controller}:_action",
  165. "${prefix}:_controller:${action}",
  166. "${prefix}:_controller:_action",
  167. "_prefix:${controller}:${action}",
  168. "_prefix:${controller}:_action",
  169. "_prefix:_controller:${action}",
  170. "_prefix:_controller:_action"
  171. ];
  172. }
  173. // Prefix and plugin has the most options
  174. // as there are 4 factors.
  175. return [
  176. "${prefix}:${plugin}.${controller}:${action}",
  177. "${prefix}:${plugin}.${controller}:_action",
  178. "${prefix}:${plugin}._controller:${action}",
  179. "${prefix}:${plugin}._controller:_action",
  180. "${prefix}:_plugin.${controller}:${action}",
  181. "${prefix}:_plugin.${controller}:_action",
  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. ];
  193. }
  194. /**
  195. * Reverse route or match a $url array with the defined routes.
  196. * Returns either the string URL generate by the route, or false on failure.
  197. *
  198. * @param array $url The url to match.
  199. * @param array $context The request context to use. Contains _base, _port,
  200. * _host, and _scheme keys.
  201. * @return string|false Either a string on match, or false on failure.
  202. * @throws \Cake\Routing\Exception\MissingRouteException when a route cannot be matched.
  203. */
  204. public function match($url, $context) {
  205. // Named routes support optimization.
  206. if (isset($url['_name'])) {
  207. $name = $url['_name'];
  208. unset($url['_name']);
  209. $out = false;
  210. if (isset($this->_named[$name])) {
  211. $route = $this->_named[$name];
  212. $out = $route->match($url + $route->defaults, $context);
  213. }
  214. if ($out) {
  215. return $out;
  216. }
  217. throw new MissingRouteException(['url' => $name]);
  218. }
  219. foreach ($this->_getNames($url) as $name) {
  220. if (empty($this->_routeTable[$name])) {
  221. continue;
  222. }
  223. foreach ($this->_routeTable[$name] as $route) {
  224. $match = $route->match($url, $context);
  225. if ($match) {
  226. return strlen($match) > 1 ? trim($match, '/') : $match;
  227. }
  228. }
  229. }
  230. throw new MissingRouteException(['url' => var_export($url, true)]);
  231. }
  232. /**
  233. * Get all the connected routes as a flat list.
  234. *
  235. * @return array
  236. */
  237. public function routes() {
  238. return $this->_routes;
  239. }
  240. /**
  241. * Get the connected named routes.
  242. *
  243. * @return array
  244. */
  245. public function named() {
  246. return $this->_named;
  247. }
  248. /**
  249. * Get/set the extensions that the route collection could handle.
  250. *
  251. * @param null|string|array $extensions Either the list of extensions to set,
  252. * or null to get.
  253. * @param bool $merge Whether to merge with or override existing extensions.
  254. * Defaults to `true`.
  255. * @return array The valid extensions.
  256. */
  257. public function extensions($extensions = null, $merge = true) {
  258. if ($extensions === null) {
  259. return $this->_extensions;
  260. }
  261. $extensions = (array)$extensions;
  262. if ($merge) {
  263. $extensions = array_unique(array_merge(
  264. $this->_extensions,
  265. $extensions
  266. ));
  267. }
  268. return $this->_extensions = $extensions;
  269. }
  270. }