1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Routing;
16:
17: use BadMethodCallException;
18: use Cake\Core\App;
19: use Cake\Core\Exception\MissingPluginException;
20: use Cake\Core\Plugin;
21: use Cake\Routing\Route\Route;
22: use Cake\Utility\Inflector;
23: use InvalidArgumentException;
24: use RuntimeException;
25:
26: /**
27: * Provides features for building routes inside scopes.
28: *
29: * Gives an easy to use way to build routes and append them
30: * into a route collection.
31: */
32: class RouteBuilder
33: {
34: /**
35: * Regular expression for auto increment IDs
36: *
37: * @var string
38: */
39: const ID = '[0-9]+';
40:
41: /**
42: * Regular expression for UUIDs
43: *
44: * @var string
45: */
46: 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}';
47:
48: /**
49: * Default HTTP request method => controller action map.
50: *
51: * @var array
52: */
53: protected static $_resourceMap = [
54: 'index' => ['action' => 'index', 'method' => 'GET', 'path' => ''],
55: 'create' => ['action' => 'add', 'method' => 'POST', 'path' => ''],
56: 'view' => ['action' => 'view', 'method' => 'GET', 'path' => ':id'],
57: 'update' => ['action' => 'edit', 'method' => ['PUT', 'PATCH'], 'path' => ':id'],
58: 'delete' => ['action' => 'delete', 'method' => 'DELETE', 'path' => ':id'],
59: ];
60:
61: /**
62: * Default route class to use if none is provided in connect() options.
63: *
64: * @var string
65: */
66: protected $_routeClass = 'Cake\Routing\Route\Route';
67:
68: /**
69: * The extensions that should be set into the routes connected.
70: *
71: * @var string[]
72: */
73: protected $_extensions = [];
74:
75: /**
76: * The path prefix scope that this collection uses.
77: *
78: * @var string
79: */
80: protected $_path;
81:
82: /**
83: * The scope parameters if there are any.
84: *
85: * @var array
86: */
87: protected $_params;
88:
89: /**
90: * Name prefix for connected routes.
91: *
92: * @var string
93: */
94: protected $_namePrefix = '';
95:
96: /**
97: * The route collection routes should be added to.
98: *
99: * @var \Cake\Routing\RouteCollection
100: */
101: protected $_collection;
102:
103: /**
104: * The list of middleware that routes in this builder get
105: * added during construction.
106: *
107: * @var array
108: */
109: protected $middleware = [];
110:
111: /**
112: * Constructor
113: *
114: * ### Options
115: *
116: * - `routeClass` - The default route class to use when adding routes.
117: * - `extensions` - The extensions to connect when adding routes.
118: * - `namePrefix` - The prefix to prepend to all route names.
119: * - `middleware` - The names of the middleware routes should have applied.
120: *
121: * @param \Cake\Routing\RouteCollection $collection The route collection to append routes into.
122: * @param string $path The path prefix the scope is for.
123: * @param array $params The scope's routing parameters.
124: * @param array $options Options list.
125: */
126: public function __construct(RouteCollection $collection, $path, array $params = [], array $options = [])
127: {
128: $this->_collection = $collection;
129: $this->_path = $path;
130: $this->_params = $params;
131: if (isset($options['routeClass'])) {
132: $this->_routeClass = $options['routeClass'];
133: }
134: if (isset($options['extensions'])) {
135: $this->_extensions = $options['extensions'];
136: }
137: if (isset($options['namePrefix'])) {
138: $this->_namePrefix = $options['namePrefix'];
139: }
140: if (isset($options['middleware'])) {
141: $this->middleware = (array)$options['middleware'];
142: }
143: }
144:
145: /**
146: * Get or set default route class.
147: *
148: * @deprecated 3.5.0 Use getRouteClass/setRouteClass instead.
149: * @param string|null $routeClass Class name.
150: * @return string|null
151: */
152: public function routeClass($routeClass = null)
153: {
154: deprecationWarning(
155: 'RouteBuilder::routeClass() is deprecated. ' .
156: 'Use RouteBuilder::setRouteClass()/getRouteClass() instead.'
157: );
158: if ($routeClass === null) {
159: return $this->getRouteClass();
160: }
161: $this->setRouteClass($routeClass);
162: }
163:
164: /**
165: * Set default route class.
166: *
167: * @param string $routeClass Class name.
168: * @return $this
169: */
170: public function setRouteClass($routeClass)
171: {
172: $this->_routeClass = $routeClass;
173:
174: return $this;
175: }
176:
177: /**
178: * Get default route class.
179: *
180: * @return string
181: */
182: public function getRouteClass()
183: {
184: return $this->_routeClass;
185: }
186:
187: /**
188: * Get or set the extensions in this route builder's scope.
189: *
190: * Future routes connected in through this builder will have the connected
191: * extensions applied. However, setting extensions does not modify existing routes.
192: *
193: * @deprecated 3.5.0 Use getExtensions/setExtensions instead.
194: * @param array|string|null $extensions Either the extensions to use or null.
195: * @return array|null
196: */
197: public function extensions($extensions = null)
198: {
199: deprecationWarning(
200: 'RouteBuilder::extensions() is deprecated. ' .
201: 'Use RouteBuilder::setExtensions()/getExtensions() instead.'
202: );
203: if ($extensions === null) {
204: return $this->getExtensions();
205: }
206: $this->setExtensions($extensions);
207: }
208:
209: /**
210: * Set the extensions in this route builder's scope.
211: *
212: * Future routes connected in through this builder will have the connected
213: * extensions applied. However, setting extensions does not modify existing routes.
214: *
215: * @param string|string[] $extensions The extensions to set.
216: * @return $this
217: */
218: public function setExtensions($extensions)
219: {
220: $this->_extensions = (array)$extensions;
221:
222: return $this;
223: }
224:
225: /**
226: * Get the extensions in this route builder's scope.
227: *
228: * @return string[]
229: */
230: public function getExtensions()
231: {
232: return $this->_extensions;
233: }
234:
235: /**
236: * Add additional extensions to what is already in current scope
237: *
238: * @param string|array $extensions One or more extensions to add
239: * @return void
240: */
241: public function addExtensions($extensions)
242: {
243: $extensions = array_merge($this->_extensions, (array)$extensions);
244: $this->_extensions = array_unique($extensions);
245: }
246:
247: /**
248: * Get the path this scope is for.
249: *
250: * @return string
251: */
252: public function path()
253: {
254: $routeKey = strpos($this->_path, ':');
255: if ($routeKey !== false) {
256: return substr($this->_path, 0, $routeKey);
257: }
258:
259: return $this->_path;
260: }
261:
262: /**
263: * Get the parameter names/values for this scope.
264: *
265: * @return array
266: */
267: public function params()
268: {
269: return $this->_params;
270: }
271:
272: /**
273: * Checks if there is already a route with a given name.
274: *
275: * @param string $name Name.
276: * @return bool
277: */
278: public function nameExists($name)
279: {
280: return array_key_exists($name, $this->_collection->named());
281: }
282:
283: /**
284: * Get/set the name prefix for this scope.
285: *
286: * Modifying the name prefix will only change the prefix
287: * used for routes connected after the prefix is changed.
288: *
289: * @param string|null $value Either the value to set or null.
290: * @return string
291: */
292: public function namePrefix($value = null)
293: {
294: if ($value !== null) {
295: $this->_namePrefix = $value;
296: }
297:
298: return $this->_namePrefix;
299: }
300:
301: /**
302: * Generate REST resource routes for the given controller(s).
303: *
304: * A quick way to generate a default routes to a set of REST resources (controller(s)).
305: *
306: * ### Usage
307: *
308: * Connect resource routes for an app controller:
309: *
310: * ```
311: * $routes->resources('Posts');
312: * ```
313: *
314: * Connect resource routes for the Comments controller in the
315: * Comments plugin:
316: *
317: * ```
318: * Router::plugin('Comments', function ($routes) {
319: * $routes->resources('Comments');
320: * });
321: * ```
322: *
323: * Plugins will create lower_case underscored resource routes. e.g
324: * `/comments/comments`
325: *
326: * Connect resource routes for the Articles controller in the
327: * Admin prefix:
328: *
329: * ```
330: * Router::prefix('admin', function ($routes) {
331: * $routes->resources('Articles');
332: * });
333: * ```
334: *
335: * Prefixes will create lower_case underscored resource routes. e.g
336: * `/admin/posts`
337: *
338: * You can create nested resources by passing a callback in:
339: *
340: * ```
341: * $routes->resources('Articles', function ($routes) {
342: * $routes->resources('Comments');
343: * });
344: * ```
345: *
346: * The above would generate both resource routes for `/articles`, and `/articles/:article_id/comments`.
347: * You can use the `map` option to connect additional resource methods:
348: *
349: * ```
350: * $routes->resources('Articles', [
351: * 'map' => ['deleteAll' => ['action' => 'deleteAll', 'method' => 'DELETE']]
352: * ]);
353: * ```
354: *
355: * In addition to the default routes, this would also connect a route for `/articles/delete_all`.
356: * By default the path segment will match the key name. You can use the 'path' key inside the resource
357: * definition to customize the path name.
358: *
359: * You can use the `inflect` option to change how path segments are generated:
360: *
361: * ```
362: * $routes->resources('PaymentTypes', ['inflect' => 'dasherize']);
363: * ```
364: *
365: * Will generate routes like `/payment-types` instead of `/payment_types`
366: *
367: * ### Options:
368: *
369: * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
370: * integer values and UUIDs.
371: * - 'inflect' - Choose the inflection method used on the resource name. Defaults to 'underscore'.
372: * - 'only' - Only connect the specific list of actions.
373: * - 'actions' - Override the method names used for connecting actions.
374: * - 'map' - Additional resource routes that should be connected. If you define 'only' and 'map',
375: * make sure that your mapped methods are also in the 'only' list.
376: * - 'prefix' - Define a routing prefix for the resource controller. If the current scope
377: * defines a prefix, this prefix will be appended to it.
378: * - 'connectOptions' - Custom options for connecting the routes.
379: * - 'path' - Change the path so it doesn't match the resource name. E.g ArticlesController
380: * is available at `/posts`
381: *
382: * @param string $name A controller name to connect resource routes for.
383: * @param array|callable $options Options to use when generating REST routes, or a callback.
384: * @param callable|null $callback An optional callback to be executed in a nested scope. Nested
385: * scopes inherit the existing path and 'id' parameter.
386: * @return void
387: */
388: public function resources($name, $options = [], $callback = null)
389: {
390: if (is_callable($options)) {
391: $callback = $options;
392: $options = [];
393: }
394: $options += [
395: 'connectOptions' => [],
396: 'inflect' => 'underscore',
397: 'id' => static::ID . '|' . static::UUID,
398: 'only' => [],
399: 'actions' => [],
400: 'map' => [],
401: 'prefix' => null,
402: 'path' => null,
403: ];
404:
405: foreach ($options['map'] as $k => $mapped) {
406: $options['map'][$k] += ['method' => 'GET', 'path' => $k, 'action' => ''];
407: }
408:
409: $ext = null;
410: if (!empty($options['_ext'])) {
411: $ext = $options['_ext'];
412: }
413:
414: $connectOptions = $options['connectOptions'];
415: if (empty($options['path'])) {
416: $method = $options['inflect'];
417: $options['path'] = Inflector::$method($name);
418: }
419: $resourceMap = array_merge(static::$_resourceMap, $options['map']);
420:
421: $only = (array)$options['only'];
422: if (empty($only)) {
423: $only = array_keys($resourceMap);
424: }
425:
426: $prefix = '';
427: if ($options['prefix']) {
428: $prefix = $options['prefix'];
429: }
430: if (isset($this->_params['prefix']) && $prefix) {
431: $prefix = $this->_params['prefix'] . '/' . $prefix;
432: }
433:
434: foreach ($resourceMap as $method => $params) {
435: if (!in_array($method, $only, true)) {
436: continue;
437: }
438:
439: $action = $params['action'];
440: if (isset($options['actions'][$method])) {
441: $action = $options['actions'][$method];
442: }
443:
444: $url = '/' . implode('/', array_filter([$options['path'], $params['path']]));
445: $params = [
446: 'controller' => $name,
447: 'action' => $action,
448: '_method' => $params['method'],
449: ];
450: if ($prefix) {
451: $params['prefix'] = $prefix;
452: }
453: $routeOptions = $connectOptions + [
454: 'id' => $options['id'],
455: 'pass' => ['id'],
456: '_ext' => $ext,
457: ];
458: $this->connect($url, $params, $routeOptions);
459: }
460:
461: if (is_callable($callback)) {
462: $idName = Inflector::singularize(Inflector::underscore($name)) . '_id';
463: $path = '/' . $options['path'] . '/:' . $idName;
464: $this->scope($path, [], $callback);
465: }
466: }
467:
468: /**
469: * Create a route that only responds to GET requests.
470: *
471: * @param string $template The URL template to use.
472: * @param array $target An array describing the target route parameters. These parameters
473: * should indicate the plugin, prefix, controller, and action that this route points to.
474: * @param string $name The name of the route.
475: * @return \Cake\Routing\Route\Route
476: */
477: public function get($template, $target, $name = null)
478: {
479: return $this->_methodRoute('GET', $template, $target, $name);
480: }
481:
482: /**
483: * Create a route that only responds to POST requests.
484: *
485: * @param string $template The URL template to use.
486: * @param array $target An array describing the target route parameters. These parameters
487: * should indicate the plugin, prefix, controller, and action that this route points to.
488: * @param string $name The name of the route.
489: * @return \Cake\Routing\Route\Route
490: */
491: public function post($template, $target, $name = null)
492: {
493: return $this->_methodRoute('POST', $template, $target, $name);
494: }
495:
496: /**
497: * Create a route that only responds to PUT requests.
498: *
499: * @param string $template The URL template to use.
500: * @param array $target An array describing the target route parameters. These parameters
501: * should indicate the plugin, prefix, controller, and action that this route points to.
502: * @param string $name The name of the route.
503: * @return \Cake\Routing\Route\Route
504: */
505: public function put($template, $target, $name = null)
506: {
507: return $this->_methodRoute('PUT', $template, $target, $name);
508: }
509:
510: /**
511: * Create a route that only responds to PATCH requests.
512: *
513: * @param string $template The URL template to use.
514: * @param array $target An array describing the target route parameters. These parameters
515: * should indicate the plugin, prefix, controller, and action that this route points to.
516: * @param string $name The name of the route.
517: * @return \Cake\Routing\Route\Route
518: */
519: public function patch($template, $target, $name = null)
520: {
521: return $this->_methodRoute('PATCH', $template, $target, $name);
522: }
523:
524: /**
525: * Create a route that only responds to DELETE requests.
526: *
527: * @param string $template The URL template to use.
528: * @param array $target An array describing the target route parameters. These parameters
529: * should indicate the plugin, prefix, controller, and action that this route points to.
530: * @param string $name The name of the route.
531: * @return \Cake\Routing\Route\Route
532: */
533: public function delete($template, $target, $name = null)
534: {
535: return $this->_methodRoute('DELETE', $template, $target, $name);
536: }
537:
538: /**
539: * Create a route that only responds to HEAD requests.
540: *
541: * @param string $template The URL template to use.
542: * @param array $target An array describing the target route parameters. These parameters
543: * should indicate the plugin, prefix, controller, and action that this route points to.
544: * @param string $name The name of the route.
545: * @return \Cake\Routing\Route\Route
546: */
547: public function head($template, $target, $name = null)
548: {
549: return $this->_methodRoute('HEAD', $template, $target, $name);
550: }
551:
552: /**
553: * Create a route that only responds to OPTIONS requests.
554: *
555: * @param string $template The URL template to use.
556: * @param array $target An array describing the target route parameters. These parameters
557: * should indicate the plugin, prefix, controller, and action that this route points to.
558: * @param string $name The name of the route.
559: * @return \Cake\Routing\Route\Route
560: */
561: public function options($template, $target, $name = null)
562: {
563: return $this->_methodRoute('OPTIONS', $template, $target, $name);
564: }
565:
566: /**
567: * Helper to create routes that only respond to a single HTTP method.
568: *
569: * @param string $method The HTTP method name to match.
570: * @param string $template The URL template to use.
571: * @param array $target An array describing the target route parameters. These parameters
572: * should indicate the plugin, prefix, controller, and action that this route points to.
573: * @param string $name The name of the route.
574: * @return \Cake\Routing\Route\Route
575: */
576: protected function _methodRoute($method, $template, $target, $name)
577: {
578: if ($name !== null) {
579: $name = $this->_namePrefix . $name;
580: }
581: $options = [
582: '_name' => $name,
583: '_ext' => $this->_extensions,
584: '_middleware' => $this->middleware,
585: 'routeClass' => $this->_routeClass,
586: ];
587:
588: $target = $this->parseDefaults($target);
589: $target['_method'] = $method;
590:
591: $route = $this->_makeRoute($template, $target, $options);
592: $this->_collection->add($route, $options);
593:
594: return $route;
595: }
596:
597: /**
598: * Load routes from a plugin.
599: *
600: * The routes file will have a local variable named `$routes` made available which contains
601: * the current RouteBuilder instance.
602: *
603: * @param string $name The plugin name
604: * @param string $file The routes file to load. Defaults to `routes.php`. This parameter
605: * is deprecated and will be removed in 4.0
606: * @return void
607: * @throws \Cake\Core\Exception\MissingPluginException When the plugin has not been loaded.
608: * @throws \InvalidArgumentException When the plugin does not have a routes file.
609: */
610: public function loadPlugin($name, $file = 'routes.php')
611: {
612: $plugins = Plugin::getCollection();
613: if (!$plugins->has($name)) {
614: throw new MissingPluginException(['plugin' => $name]);
615: }
616: $plugin = $plugins->get($name);
617:
618: // @deprecated This block should be removed in 4.0
619: if ($file !== 'routes.php') {
620: deprecationWarning(
621: 'Loading plugin routes now uses the routes() hook method on the plugin class. ' .
622: 'Loading non-standard files will be removed in 4.0'
623: );
624:
625: $path = $plugin->getConfigPath() . DIRECTORY_SEPARATOR . $file;
626: if (!file_exists($path)) {
627: throw new InvalidArgumentException(sprintf(
628: 'Cannot load routes for the plugin named %s. The %s file does not exist.',
629: $name,
630: $path
631: ));
632: }
633:
634: $routes = $this;
635: include $path;
636:
637: return;
638: }
639: $plugin->routes($this);
640:
641: // Disable the routes hook to prevent duplicate route issues.
642: $plugin->disable('routes');
643: }
644:
645: /**
646: * Connects a new Route.
647: *
648: * Routes are a way of connecting request URLs to objects in your application.
649: * At their core routes are a set or regular expressions that are used to
650: * match requests to destinations.
651: *
652: * Examples:
653: *
654: * ```
655: * $routes->connect('/:controller/:action/*');
656: * ```
657: *
658: * The first parameter will be used as a controller name while the second is
659: * used as the action name. The '/*' syntax makes this route greedy in that
660: * it will match requests like `/posts/index` as well as requests
661: * like `/posts/edit/1/foo/bar`.
662: *
663: * ```
664: * $routes->connect('/home-page', ['controller' => 'Pages', 'action' => 'display', 'home']);
665: * ```
666: *
667: * The above shows the use of route parameter defaults. And providing routing
668: * parameters for a static route.
669: *
670: * ```
671: * $routes->connect(
672: * '/:lang/:controller/:action/:id',
673: * [],
674: * ['id' => '[0-9]+', 'lang' => '[a-z]{3}']
675: * );
676: * ```
677: *
678: * Shows connecting a route with custom route parameters as well as
679: * providing patterns for those parameters. Patterns for routing parameters
680: * do not need capturing groups, as one will be added for each route params.
681: *
682: * $options offers several 'special' keys that have special meaning
683: * in the $options array.
684: *
685: * - `routeClass` is used to extend and change how individual routes parse requests
686: * and handle reverse routing, via a custom routing class.
687: * Ex. `'routeClass' => 'SlugRoute'`
688: * - `pass` is used to define which of the routed parameters should be shifted
689: * into the pass array. Adding a parameter to pass will remove it from the
690: * regular route array. Ex. `'pass' => ['slug']`.
691: * - `persist` is used to define which route parameters should be automatically
692: * included when generating new URLs. You can override persistent parameters
693: * by redefining them in a URL or remove them by setting the parameter to `false`.
694: * Ex. `'persist' => ['lang']`
695: * - `multibytePattern` Set to true to enable multibyte pattern support in route
696: * parameter patterns.
697: * - `_name` is used to define a specific name for routes. This can be used to optimize
698: * reverse routing lookups. If undefined a name will be generated for each
699: * connected route.
700: * - `_ext` is an array of filename extensions that will be parsed out of the url if present.
701: * See {@link \Cake\Routing\RouteCollection::setExtensions()}.
702: * - `_method` Only match requests with specific HTTP verbs.
703: *
704: * Example of using the `_method` condition:
705: *
706: * ```
707: * $routes->connect('/tasks', ['controller' => 'Tasks', 'action' => 'index', '_method' => 'GET']);
708: * ```
709: *
710: * The above route will only be matched for GET requests. POST requests will fail to match this route.
711: *
712: * @param string $route A string describing the template of the route
713: * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default
714: * and can supply routing parameters that are not dynamic. See above.
715: * @param array $options An array matching the named elements in the route to regular expressions which that
716: * element should match. Also contains additional parameters such as which routed parameters should be
717: * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
718: * custom routing class.
719: * @return \Cake\Routing\Route\Route
720: * @throws \InvalidArgumentException
721: * @throws \BadMethodCallException
722: */
723: public function connect($route, $defaults = [], array $options = [])
724: {
725: $defaults = $this->parseDefaults($defaults);
726: if (empty($options['_ext'])) {
727: $options['_ext'] = $this->_extensions;
728: }
729: if (empty($options['routeClass'])) {
730: $options['routeClass'] = $this->_routeClass;
731: }
732: if (isset($options['_name']) && $this->_namePrefix) {
733: $options['_name'] = $this->_namePrefix . $options['_name'];
734: }
735: if (empty($options['_middleware'])) {
736: $options['_middleware'] = $this->middleware;
737: }
738:
739: $route = $this->_makeRoute($route, $defaults, $options);
740: $this->_collection->add($route, $options);
741:
742: return $route;
743: }
744:
745: /**
746: * Parse the defaults if they're a string
747: *
748: * @param string|array $defaults Defaults array from the connect() method.
749: * @return string|array
750: */
751: protected static function parseDefaults($defaults)
752: {
753: if (!is_string($defaults)) {
754: return $defaults;
755: }
756:
757: $regex = '/(?:(?<plugin>[a-zA-Z0-9\/]*)\.)?(?<prefix>[a-zA-Z0-9\/]*?)' .
758: '(?:\/)?(?<controller>[a-zA-Z0-9]*):{2}(?<action>[a-zA-Z0-9_]*)/i';
759:
760: if (preg_match($regex, $defaults, $matches)) {
761: foreach ($matches as $key => $value) {
762: // Remove numeric keys and empty values.
763: if (is_int($key) || $value === '' || $value === '::') {
764: unset($matches[$key]);
765: }
766: }
767: $length = count($matches);
768:
769: if (isset($matches['prefix'])) {
770: $matches['prefix'] = strtolower($matches['prefix']);
771: }
772:
773: if ($length >= 2 || $length <= 4) {
774: return $matches;
775: }
776: }
777: throw new RuntimeException("Could not parse `{$defaults}` route destination string.");
778: }
779:
780: /**
781: * Create a route object, or return the provided object.
782: *
783: * @param string|\Cake\Routing\Route\Route $route The route template or route object.
784: * @param array $defaults Default parameters.
785: * @param array $options Additional options parameters.
786: * @return \Cake\Routing\Route\Route
787: * @throws \InvalidArgumentException when route class or route object is invalid.
788: * @throws \BadMethodCallException when the route to make conflicts with the current scope
789: */
790: protected function _makeRoute($route, $defaults, $options)
791: {
792: if (is_string($route)) {
793: $routeClass = App::className($options['routeClass'], 'Routing/Route');
794: if ($routeClass === false) {
795: throw new InvalidArgumentException(sprintf(
796: 'Cannot find route class %s',
797: $options['routeClass']
798: ));
799: }
800:
801: $route = str_replace('//', '/', $this->_path . $route);
802: if ($route !== '/') {
803: $route = rtrim($route, '/');
804: }
805:
806: foreach ($this->_params as $param => $val) {
807: if (isset($defaults[$param]) && $param !== 'prefix' && $defaults[$param] !== $val) {
808: $msg = 'You cannot define routes that conflict with the scope. ' .
809: 'Scope had %s = %s, while route had %s = %s';
810: throw new BadMethodCallException(sprintf(
811: $msg,
812: $param,
813: $val,
814: $param,
815: $defaults[$param]
816: ));
817: }
818: }
819: $defaults += $this->_params + ['plugin' => null];
820: if (!isset($defaults['action']) && !isset($options['action'])) {
821: $defaults['action'] = 'index';
822: }
823:
824: $route = new $routeClass($route, $defaults, $options);
825: }
826:
827: if ($route instanceof Route) {
828: return $route;
829: }
830: throw new InvalidArgumentException(
831: 'Route class not found, or route class is not a subclass of Cake\Routing\Route\Route'
832: );
833: }
834:
835: /**
836: * Connects a new redirection Route in the router.
837: *
838: * Redirection routes are different from normal routes as they perform an actual
839: * header redirection if a match is found. The redirection can occur within your
840: * application or redirect to an outside location.
841: *
842: * Examples:
843: *
844: * ```
845: * $routes->redirect('/home/*', ['controller' => 'posts', 'action' => 'view']);
846: * ```
847: *
848: * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the
849: * redirect destination allows you to use other routes to define where a URL string should be redirected to.
850: *
851: * ```
852: * $routes->redirect('/posts/*', 'http://google.com', ['status' => 302]);
853: * ```
854: *
855: * Redirects /posts/* to http://google.com with a HTTP status of 302
856: *
857: * ### Options:
858: *
859: * - `status` Sets the HTTP status (default 301)
860: * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes,
861: * routes that end in `*` are greedy. As you can remap URLs and not lose any passed args.
862: *
863: * @param string $route A string describing the template of the route
864: * @param array|string $url A URL to redirect to. Can be a string or a Cake array-based URL
865: * @param array $options An array matching the named elements in the route to regular expressions which that
866: * element should match. Also contains additional parameters such as which routed parameters should be
867: * shifted into the passed arguments. As well as supplying patterns for routing parameters.
868: * @return \Cake\Routing\Route\Route|\Cake\Routing\Route\RedirectRoute
869: */
870: public function redirect($route, $url, array $options = [])
871: {
872: if (!isset($options['routeClass'])) {
873: $options['routeClass'] = 'Cake\Routing\Route\RedirectRoute';
874: }
875: if (is_string($url)) {
876: $url = ['redirect' => $url];
877: }
878:
879: return $this->connect($route, $url, $options);
880: }
881:
882: /**
883: * Add prefixed routes.
884: *
885: * This method creates a scoped route collection that includes
886: * relevant prefix information.
887: *
888: * The $name parameter is used to generate the routing parameter name.
889: * For example a path of `admin` would result in `'prefix' => 'admin'` being
890: * applied to all connected routes.
891: *
892: * You can re-open a prefix as many times as necessary, as well as nest prefixes.
893: * Nested prefixes will result in prefix values like `admin/api` which translates
894: * to the `Controller\Admin\Api\` namespace.
895: *
896: * If you need to have prefix with dots, eg: '/api/v1.0', use 'path' key
897: * for $params argument:
898: *
899: * ```
900: * $route->prefix('api', function($route) {
901: * $route->prefix('v10', ['path' => '/v1.0'], function($route) {
902: * // Translates to `Controller\Api\V10\` namespace
903: * });
904: * });
905: * ```
906: *
907: * @param string $name The prefix name to use.
908: * @param array|callable $params An array of routing defaults to add to each connected route.
909: * If you have no parameters, this argument can be a callable.
910: * @param callable|null $callback The callback to invoke that builds the prefixed routes.
911: * @return void
912: * @throws \InvalidArgumentException If a valid callback is not passed
913: */
914: public function prefix($name, $params = [], callable $callback = null)
915: {
916: if ($callback === null) {
917: if (!is_callable($params)) {
918: throw new InvalidArgumentException('A valid callback is expected');
919: }
920: $callback = $params;
921: $params = [];
922: }
923: $name = Inflector::underscore($name);
924: $path = '/' . $name;
925: if (isset($params['path'])) {
926: $path = $params['path'];
927: unset($params['path']);
928: }
929: if (isset($this->_params['prefix'])) {
930: $name = $this->_params['prefix'] . '/' . $name;
931: }
932: $params = array_merge($params, ['prefix' => $name]);
933: $this->scope($path, $params, $callback);
934: }
935:
936: /**
937: * Add plugin routes.
938: *
939: * This method creates a new scoped route collection that includes
940: * relevant plugin information.
941: *
942: * The plugin name will be inflected to the underscore version to create
943: * the routing path. If you want a custom path name, use the `path` option.
944: *
945: * Routes connected in the scoped collection will have the correct path segment
946: * prepended, and have a matching plugin routing key set.
947: *
948: * @param string $name The plugin name to build routes for
949: * @param array|callable $options Either the options to use, or a callback
950: * @param callable|null $callback The callback to invoke that builds the plugin routes
951: * Only required when $options is defined.
952: * @return void
953: */
954: public function plugin($name, $options = [], $callback = null)
955: {
956: if ($callback === null) {
957: $callback = $options;
958: $options = [];
959: }
960: $params = ['plugin' => $name] + $this->_params;
961: if (empty($options['path'])) {
962: $options['path'] = '/' . Inflector::underscore($name);
963: }
964: $this->scope($options['path'], $params, $callback);
965: }
966:
967: /**
968: * Create a new routing scope.
969: *
970: * Scopes created with this method will inherit the properties of the scope they are
971: * added to. This means that both the current path and parameters will be appended
972: * to the supplied parameters.
973: *
974: * @param string $path The path to create a scope for.
975: * @param array|callable $params Either the parameters to add to routes, or a callback.
976: * @param callable|null $callback The callback to invoke that builds the plugin routes.
977: * Only required when $params is defined.
978: * @return void
979: * @throws \InvalidArgumentException when there is no callable parameter.
980: */
981: public function scope($path, $params, $callback = null)
982: {
983: if (is_callable($params)) {
984: $callback = $params;
985: $params = [];
986: }
987: if (!is_callable($callback)) {
988: $msg = 'Need a callable function/object to connect routes.';
989: throw new InvalidArgumentException($msg);
990: }
991:
992: if ($this->_path !== '/') {
993: $path = $this->_path . $path;
994: }
995: $namePrefix = $this->_namePrefix;
996: if (isset($params['_namePrefix'])) {
997: $namePrefix .= $params['_namePrefix'];
998: }
999: unset($params['_namePrefix']);
1000:
1001: $params += $this->_params;
1002: $builder = new static($this->_collection, $path, $params, [
1003: 'routeClass' => $this->_routeClass,
1004: 'extensions' => $this->_extensions,
1005: 'namePrefix' => $namePrefix,
1006: 'middleware' => $this->middleware,
1007: ]);
1008: $callback($builder);
1009: }
1010:
1011: /**
1012: * Connect the `/:controller` and `/:controller/:action/*` fallback routes.
1013: *
1014: * This is a shortcut method for connecting fallback routes in a given scope.
1015: *
1016: * @param string|null $routeClass the route class to use, uses the default routeClass
1017: * if not specified
1018: * @return void
1019: */
1020: public function fallbacks($routeClass = null)
1021: {
1022: $routeClass = $routeClass ?: $this->_routeClass;
1023: $this->connect('/:controller', ['action' => 'index'], compact('routeClass'));
1024: $this->connect('/:controller/:action/*', [], compact('routeClass'));
1025: }
1026:
1027: /**
1028: * Register a middleware with the RouteCollection.
1029: *
1030: * Once middleware has been registered, it can be applied to the current routing
1031: * scope or any child scopes that share the same RouteCollection.
1032: *
1033: * @param string $name The name of the middleware. Used when applying middleware to a scope.
1034: * @param callable|string $middleware The middleware callable or class name to register.
1035: * @return $this
1036: * @see \Cake\Routing\RouteCollection
1037: */
1038: public function registerMiddleware($name, $middleware)
1039: {
1040: $this->_collection->registerMiddleware($name, $middleware);
1041:
1042: return $this;
1043: }
1044:
1045: /**
1046: * Apply a middleware to the current route scope.
1047: *
1048: * Requires middleware to be registered via `registerMiddleware()`
1049: *
1050: * @param string ...$names The names of the middleware to apply to the current scope.
1051: * @return $this
1052: * @see \Cake\Routing\RouteCollection::addMiddlewareToScope()
1053: */
1054: public function applyMiddleware(...$names)
1055: {
1056: foreach ($names as $name) {
1057: if (!$this->_collection->middlewareExists($name)) {
1058: $message = "Cannot apply '$name' middleware or middleware group. " .
1059: 'Use registerMiddleware() to register middleware.';
1060: throw new RuntimeException($message);
1061: }
1062: }
1063: $this->middleware = array_unique(array_merge($this->middleware, $names));
1064:
1065: return $this;
1066: }
1067:
1068: /**
1069: * Apply a set of middleware to a group
1070: *
1071: * @param string $name Name of the middleware group
1072: * @param string[] $middlewareNames Names of the middleware
1073: * @return $this
1074: */
1075: public function middlewareGroup($name, array $middlewareNames)
1076: {
1077: $this->_collection->middlewareGroup($name, $middlewareNames);
1078:
1079: return $this;
1080: }
1081: }
1082: