RouteBuilder.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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 BadMethodCallException;
  17. use Cake\Core\App;
  18. use Cake\Routing\Router;
  19. use Cake\Routing\Route\Route;
  20. use Cake\Utility\Inflector;
  21. use InvalidArgumentException;
  22. /**
  23. * Provides features for building routes inside scopes.
  24. *
  25. * Gives an easy to use way to build routes and append them
  26. * into a route collection.
  27. */
  28. class RouteBuilder {
  29. /**
  30. * Regular expression for auto increment IDs
  31. *
  32. * @var string
  33. */
  34. const ID = '[0-9]+';
  35. /**
  36. * Regular expression for UUIDs
  37. *
  38. * @var string
  39. */
  40. const UUID = '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}';
  41. /**
  42. * Default HTTP request method => controller action map.
  43. *
  44. * @var array
  45. */
  46. protected static $_resourceMap = [
  47. 'index' => ['action' => 'index', 'method' => 'GET', 'path' => ''],
  48. 'create' => ['action' => 'add', 'method' => 'POST', 'path' => ''],
  49. 'view' => ['action' => 'view', 'method' => 'GET', 'path' => ':id'],
  50. 'update' => ['action' => 'edit', 'method' => ['PUT', 'PATCH'], 'path' => ':id'],
  51. 'delete' => ['action' => 'delete', 'method' => 'DELETE', 'path' => ':id'],
  52. ];
  53. /**
  54. * Default route class to use if none is provided in connect() options.
  55. *
  56. * @var string
  57. */
  58. protected $_routeClass = 'Cake\Routing\Route\Route';
  59. /**
  60. * The extensions that should be set into the routes connected.
  61. *
  62. * @var array
  63. */
  64. protected $_extensions = [];
  65. /**
  66. * The path prefix scope that this collection uses.
  67. *
  68. * @var string
  69. */
  70. protected $_path;
  71. /**
  72. * The scope parameters if there are any.
  73. *
  74. * @var array
  75. */
  76. protected $_params;
  77. /**
  78. * The route collection routes should be added to.
  79. *
  80. * @var Cake\Routing\RouteCollection
  81. */
  82. protected $_collection;
  83. /**
  84. * Constructor
  85. *
  86. * @param \Cake\Routing\RouteCollection $collection The route collection to append routes into.
  87. * @param string $path The path prefix the scope is for.
  88. * @param array $params The scope's routing parameters.
  89. * @param array $options Options list. Valid keys are:
  90. *
  91. * - `routeClass` - The default route class to use when adding routes.
  92. * - `extensions` - The extensions to connect when adding routes.
  93. */
  94. public function __construct($collection, $path, array $params = [], array $options = []) {
  95. $this->_collection = $collection;
  96. $this->_path = $path;
  97. $this->_params = $params;
  98. if (isset($options['routeClass'])) {
  99. $this->_routeClass = $options['routeClass'];
  100. }
  101. if (isset($options['extensions'])) {
  102. $this->_extensions = $options['extensions'];
  103. }
  104. }
  105. /**
  106. * Get or set default route class.
  107. *
  108. * @param string|null $routeClass Class name.
  109. * @return string|void
  110. */
  111. public function routeClass($routeClass = null) {
  112. if ($routeClass == null) {
  113. return $this->_routeClass;
  114. }
  115. $this->_routeClass = $routeClass;
  116. }
  117. /**
  118. * Get or set the extensions in this route collection.
  119. *
  120. * Setting extensions does not modify existing routes.
  121. *
  122. * @param null|string|array $extensions Either the extensions to use or null.
  123. * @return array|void
  124. */
  125. public function extensions($extensions = null) {
  126. if ($extensions === null) {
  127. return $this->_extensions;
  128. }
  129. $this->_extensions = (array)$extensions;
  130. }
  131. /**
  132. * Get the path this scope is for.
  133. *
  134. * @return string
  135. */
  136. public function path() {
  137. $routeKey = strpos($this->_path, ':');
  138. if ($routeKey !== false) {
  139. return substr($this->_path, 0, $routeKey);
  140. }
  141. return $this->_path;
  142. }
  143. /**
  144. * Get the parameter names/values for this scope.
  145. *
  146. * @return string
  147. */
  148. public function params() {
  149. return $this->_params;
  150. }
  151. /**
  152. * Generate REST resource routes for the given controller(s).
  153. *
  154. * A quick way to generate a default routes to a set of REST resources (controller(s)).
  155. *
  156. * ### Usage
  157. *
  158. * Connect resource routes for an app controller:
  159. *
  160. * {{{
  161. * $routes->resources('Posts');
  162. * }}}
  163. *
  164. * Connect resource routes for the Comments controller in the
  165. * Comments plugin:
  166. *
  167. * {{{
  168. * Router::plugin('Comments', function ($routes) {
  169. * $routes->resources('Comments');
  170. * });
  171. * }}}
  172. *
  173. * Plugins will create lower_case underscored resource routes. e.g
  174. * `/comments/comments`
  175. *
  176. * Connect resource routes for the Articles controller in the
  177. * Admin prefix:
  178. *
  179. * {{{
  180. * Router::prefix('admin', function ($routes) {
  181. * $routes->resources('Articles');
  182. * });
  183. * }}}
  184. *
  185. * Prefixes will create lower_case underscored resource routes. e.g
  186. * `/admin/posts`
  187. *
  188. * You can create nested resources by passing a callback in:
  189. *
  190. * {{{
  191. * $routes->resources('Articles', function($routes) {
  192. * $routes->resources('Comments');
  193. * });
  194. * }}}
  195. *
  196. * The above would generate both resource routes for `/articles`, and `/articles/:article_id/comments`.
  197. *
  198. * ### Options:
  199. *
  200. * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
  201. * integer values and UUIDs.
  202. * - 'only' - Only connect the specific list of actions.
  203. * - 'actions' - Override the method names used for connecting actions.
  204. *
  205. * @param string $name A controller name to connect resource routes for.
  206. * @param array|callable $options Options to use when generating REST routes, or a callback.
  207. * @param callable $callback An optional callback to be executed in a nested scope. Nested
  208. * scopes inherit the existing path and 'id' parameter.
  209. * @return array Array of mapped resources
  210. */
  211. public function resources($name, $options = [], $callback = null) {
  212. if (is_callable($options) && $callback === null) {
  213. $callback = $options;
  214. $options = [];
  215. }
  216. $options += array(
  217. 'connectOptions' => [],
  218. 'id' => static::ID . '|' . static::UUID,
  219. 'only' => ['index', 'update', 'create', 'view', 'delete'],
  220. 'actions' => [],
  221. );
  222. $options['only'] = (array)$options['only'];
  223. $connectOptions = $options['connectOptions'];
  224. $urlName = Inflector::underscore($name);
  225. $ext = null;
  226. if (!empty($options['_ext'])) {
  227. $ext = $options['_ext'];
  228. }
  229. foreach (static::$_resourceMap as $method => $params) {
  230. if (!in_array($method, $options['only'], true)) {
  231. continue;
  232. }
  233. $action = $params['action'];
  234. if (isset($options['actions'][$method])) {
  235. $action = $options['actions'][$method];
  236. }
  237. $url = '/' . implode('/', array_filter([$urlName, $params['path']]));
  238. $params = array(
  239. 'controller' => $name,
  240. 'action' => $action,
  241. '_method' => $params['method'],
  242. '_ext' => $ext
  243. );
  244. $routeOptions = $connectOptions + [
  245. 'id' => $options['id'],
  246. 'pass' => ['id']
  247. ];
  248. $this->connect($url, $params, $routeOptions);
  249. }
  250. if (is_callable($callback)) {
  251. $idName = Inflector::singularize($urlName) . '_id';
  252. $path = '/' . $urlName . '/:' . $idName;
  253. $this->scope($path, [], $callback);
  254. }
  255. }
  256. /**
  257. * Connects a new Route.
  258. *
  259. * Routes are a way of connecting request URLs to objects in your application.
  260. * At their core routes are a set or regular expressions that are used to
  261. * match requests to destinations.
  262. *
  263. * Examples:
  264. *
  265. * `$routes->connect('/:controller/:action/*');`
  266. *
  267. * The first parameter will be used as a controller name while the second is
  268. * used as the action name. The '/*' syntax makes this route greedy in that
  269. * it will match requests like `/posts/index` as well as requests
  270. * like `/posts/edit/1/foo/bar`.
  271. *
  272. * `$routes->connect('/home-page', ['controller' => 'Pages', 'action' => 'display', 'home']);`
  273. *
  274. * The above shows the use of route parameter defaults. And providing routing
  275. * parameters for a static route.
  276. *
  277. * {{{
  278. * $routes->connect(
  279. * '/:lang/:controller/:action/:id',
  280. * [],
  281. * ['id' => '[0-9]+', 'lang' => '[a-z]{3}']
  282. * );
  283. * }}}
  284. *
  285. * Shows connecting a route with custom route parameters as well as
  286. * providing patterns for those parameters. Patterns for routing parameters
  287. * do not need capturing groups, as one will be added for each route params.
  288. *
  289. * $options offers several 'special' keys that have special meaning
  290. * in the $options array.
  291. *
  292. * - `pass` is used to define which of the routed parameters should be shifted
  293. * into the pass array. Adding a parameter to pass will remove it from the
  294. * regular route array. Ex. `'pass' => array('slug')`.
  295. * - `routeClass` is used to extend and change how individual routes parse requests
  296. * and handle reverse routing, via a custom routing class.
  297. * Ex. `'routeClass' => 'SlugRoute'`
  298. * - `_name` is used to define a specific name for routes. This can be used to optimize
  299. * reverse routing lookups. If undefined a name will be generated for each
  300. * connected route.
  301. * - `_ext` is an array of filename extensions that will be parsed out of the url if present.
  302. * See {@link ScopedRouteCollection::extensions()}.
  303. * - `_method` Only match requests with specific HTTP verbs.
  304. *
  305. * Example of using the `_method` condition:
  306. *
  307. * `$routes->connect('/tasks', array('controller' => 'Tasks', 'action' => 'index', '_method' => 'GET'));`
  308. *
  309. * The above route will only be matched for GET requests. POST requests will fail to match this route.
  310. *
  311. * @param string $route A string describing the template of the route
  312. * @param array $defaults An array describing the default route parameters. These parameters will be used by default
  313. * and can supply routing parameters that are not dynamic. See above.
  314. * @param array $options An array matching the named elements in the route to regular expressions which that
  315. * element should match. Also contains additional parameters such as which routed parameters should be
  316. * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
  317. * custom routing class.
  318. * @return void
  319. * @throws \InvalidArgumentException
  320. * @throws \BadMethodCallException
  321. */
  322. public function connect($route, array $defaults = [], $options = []) {
  323. if (empty($options['action'])) {
  324. $defaults += array('action' => 'index');
  325. }
  326. if (empty($options['_ext'])) {
  327. $options['_ext'] = $this->_extensions;
  328. }
  329. if (empty($options['routeClass'])) {
  330. $options['routeClass'] = $this->_routeClass;
  331. }
  332. $route = $this->_makeRoute($route, $defaults, $options);
  333. $this->_collection->add($route, $options);
  334. }
  335. /**
  336. * Create a route object, or return the provided object.
  337. *
  338. * @param string|\Cake\Routing\Route\Route $route The route template or route object.
  339. * @param array $defaults Default parameters.
  340. * @param array $options Additional options parameters.
  341. * @return \Cake\Routing\Route\Route
  342. * @throws \InvalidArgumentException when route class or route object is invalid.
  343. * @throws \BadMethodCallException when the route to make conflicts with the current scope
  344. */
  345. protected function _makeRoute($route, $defaults, $options) {
  346. if (is_string($route)) {
  347. $routeClass = App::className($options['routeClass'], 'Routing/Route');
  348. if ($routeClass === false) {
  349. throw new InvalidArgumentException(sprintf(
  350. 'Cannot find route class %s',
  351. $options['routeClass']
  352. ));
  353. }
  354. $route = str_replace('//', '/', $this->_path . $route);
  355. $route = $route === '/' ? $route : rtrim($route, '/');
  356. foreach ($this->_params as $param => $val) {
  357. if (isset($defaults[$param]) && $defaults[$param] !== $val) {
  358. $msg = 'You cannot define routes that conflict with the scope. ' .
  359. 'Scope had %s = %s, while route had %s = %s';
  360. throw new BadMethodCallException(sprintf(
  361. $msg,
  362. $param,
  363. $val,
  364. $param,
  365. $defaults[$param]
  366. ));
  367. }
  368. }
  369. $defaults += $this->_params;
  370. $defaults += ['plugin' => null];
  371. $route = new $routeClass($route, $defaults, $options);
  372. }
  373. if ($route instanceof Route) {
  374. return $route;
  375. }
  376. throw new InvalidArgumentException(
  377. 'Route class not found, or route class is not a subclass of Cake\Routing\Route\Route'
  378. );
  379. }
  380. /**
  381. * Connects a new redirection Route in the router.
  382. *
  383. * Redirection routes are different from normal routes as they perform an actual
  384. * header redirection if a match is found. The redirection can occur within your
  385. * application or redirect to an outside location.
  386. *
  387. * Examples:
  388. *
  389. * `$routes->redirect('/home/*', array('controller' => 'posts', 'action' => 'view'));`
  390. *
  391. * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the
  392. * redirect destination allows you to use other routes to define where an URL string should be redirected to.
  393. *
  394. * `$routes-redirect('/posts/*', 'http://google.com', array('status' => 302));`
  395. *
  396. * Redirects /posts/* to http://google.com with a HTTP status of 302
  397. *
  398. * ### Options:
  399. *
  400. * - `status` Sets the HTTP status (default 301)
  401. * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes,
  402. * routes that end in `*` are greedy. As you can remap URLs and not loose any passed args.
  403. *
  404. * @param string $route A string describing the template of the route
  405. * @param array $url An URL to redirect to. Can be a string or a Cake array-based URL
  406. * @param array $options An array matching the named elements in the route to regular expressions which that
  407. * element should match. Also contains additional parameters such as which routed parameters should be
  408. * shifted into the passed arguments. As well as supplying patterns for routing parameters.
  409. * @return array Array of routes
  410. */
  411. public function redirect($route, $url, array $options = []) {
  412. $options['routeClass'] = 'Cake\Routing\Route\RedirectRoute';
  413. if (is_string($url)) {
  414. $url = array('redirect' => $url);
  415. }
  416. return $this->connect($route, $url, $options);
  417. }
  418. /**
  419. * Add prefixed routes.
  420. *
  421. * This method creates a scoped route collection that includes
  422. * relevant prefix information.
  423. *
  424. * The path parameter is used to generate the routing parameter name.
  425. * For example a path of `admin` would result in `'prefix' => 'admin'` being
  426. * applied to all connected routes.
  427. *
  428. * You can re-open a prefix as many times as necessary, as well as nest prefixes.
  429. * Nested prefixes will result in prefix values like `admin/api` which translates
  430. * to the `Controller\Admin\Api\` namespace.
  431. *
  432. * @param string $name The prefix name to use.
  433. * @param callable $callback The callback to invoke that builds the prefixed routes.
  434. * @return void
  435. */
  436. public function prefix($name, callable $callback) {
  437. $name = Inflector::underscore($name);
  438. $path = '/' . $name;
  439. if (isset($this->_params['prefix'])) {
  440. $name = $this->_params['prefix'] . '/' . $name;
  441. }
  442. $params = ['prefix' => $name];
  443. $this->scope($path, $params, $callback);
  444. }
  445. /**
  446. * Add plugin routes.
  447. *
  448. * This method creates a new scoped route collection that includes
  449. * relevant plugin information.
  450. *
  451. * The plugin name will be inflected to the underscore version to create
  452. * the routing path. If you want a custom path name, use the `path` option.
  453. *
  454. * Routes connected in the scoped collection will have the correct path segment
  455. * prepended, and have a matching plugin routing key set.
  456. *
  457. * @param string $name The plugin name to build routes for
  458. * @param array|callable $options Either the options to use, or a callback
  459. * @param callable $callback The callback to invoke that builds the plugin routes
  460. * Only required when $options is defined.
  461. * @return void
  462. */
  463. public function plugin($name, $options = [], $callback = null) {
  464. if ($callback === null) {
  465. $callback = $options;
  466. $options = [];
  467. }
  468. $params = ['plugin' => $name] + $this->_params;
  469. if (empty($options['path'])) {
  470. $options['path'] = '/' . Inflector::underscore($name);
  471. }
  472. $this->scope($options['path'], $params, $callback);
  473. }
  474. /**
  475. * Create a new routing scope.
  476. *
  477. * Scopes created with this method will inherit the properties of the scope they are
  478. * added to. This means that both the current path and parameters will be appended
  479. * to the supplied parameters.
  480. *
  481. * @param string $path The path to create a scope for.
  482. * @param array|callable $params Either the parameters to add to routes, or a callback.
  483. * @param callable $callback The callback to invoke that builds the plugin routes.
  484. * Only required when $params is defined.
  485. * @return void
  486. * @throws \InvalidArgumentException when there is no callable parameter.
  487. */
  488. public function scope($path, $params, $callback = null) {
  489. if ($callback === null) {
  490. $callback = $params;
  491. $params = [];
  492. }
  493. if (!is_callable($callback)) {
  494. $msg = 'Need a callable function/object to connect routes.';
  495. throw new \InvalidArgumentException($msg);
  496. }
  497. if ($this->_path !== '/') {
  498. $path = $this->_path . $path;
  499. }
  500. $params = $params + $this->_params;
  501. $builder = new static($this->_collection, $path, $params, [
  502. 'routeClass' => $this->_routeClass,
  503. 'extensions' => $this->_extensions
  504. ]);
  505. $callback($builder);
  506. }
  507. /**
  508. * Connect the `/:controller` and `/:controller/:action/*` fallback routes.
  509. *
  510. * This is a shortcut method for connecting fallback routes in a given scope.
  511. *
  512. * @return void
  513. */
  514. public function fallbacks() {
  515. $routeClass = $this->_routeClass;
  516. if ($routeClass === 'Cake\Routing\Route\Route') {
  517. $routeClass = 'InflectedRoute';
  518. }
  519. $this->connect('/:controller', ['action' => 'index'], compact('routeClass'));
  520. $this->connect('/:controller/:action/*', [], compact('routeClass'));
  521. }
  522. }