PageRenderTime 45ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/src/Routing/RouteBuilder.php

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