1: <?php namespace Laravel\Routing;
2:
3: use Closure;
4: use Laravel\Str;
5: use Laravel\Bundle;
6: use Laravel\Request;
7:
8: class Router {
9:
10: /**
11: * The route names that have been matched.
12: *
13: * @var array
14: */
15: public static $names = array();
16:
17: /**
18: * The actions that have been reverse routed.
19: *
20: * @var array
21: */
22: public static $uses = array();
23:
24: /**
25: * All of the routes that have been registered.
26: *
27: * @var array
28: */
29: public static $routes = array(
30: 'GET' => array(),
31: 'POST' => array(),
32: 'PUT' => array(),
33: 'DELETE' => array(),
34: 'PATCH' => array(),
35: 'HEAD' => array(),
36: 'OPTIONS'=> array(),
37: );
38:
39: /**
40: * All of the "fallback" routes that have been registered.
41: *
42: * @var array
43: */
44: public static $fallback = array(
45: 'GET' => array(),
46: 'POST' => array(),
47: 'PUT' => array(),
48: 'DELETE' => array(),
49: 'PATCH' => array(),
50: 'HEAD' => array(),
51: 'OPTIONS'=> array(),
52: );
53:
54: /**
55: * The current attributes being shared by routes.
56: */
57: public static $group;
58:
59: /**
60: * The "handles" clause for the bundle currently being routed.
61: *
62: * @var string
63: */
64: public static $bundle;
65:
66: /**
67: * The number of URI segments allowed as method arguments.
68: *
69: * @var int
70: */
71: public static $segments = 5;
72:
73: /**
74: * The wildcard patterns supported by the router.
75: *
76: * @var array
77: */
78: public static $patterns = array(
79: '(:num)' => '([0-9]+)',
80: '(:any)' => '([a-zA-Z0-9\.\-_%=]+)',
81: '(:segment)' => '([^/]+)',
82: '(:all)' => '(.*)',
83: );
84:
85: /**
86: * The optional wildcard patterns supported by the router.
87: *
88: * @var array
89: */
90: public static $optional = array(
91: '/(:num?)' => '(?:/([0-9]+)',
92: '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%=]+)',
93: '/(:segment?)' => '(?:/([^/]+)',
94: '/(:all?)' => '(?:/(.*)',
95: );
96:
97: /**
98: * An array of HTTP request methods.
99: *
100: * @var array
101: */
102: public static $methods = array('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS');
103:
104: /**
105: * Register a HTTPS route with the router.
106: *
107: * @param string $method
108: * @param string|array $route
109: * @param mixed $action
110: * @return void
111: */
112: public static function secure($method, $route, $action)
113: {
114: $action = static::action($action);
115:
116: $action['https'] = true;
117:
118: static::register($method, $route, $action);
119: }
120:
121: /**
122: * Register many request URIs to a single action.
123: *
124: * <code>
125: * // Register a group of URIs for an action
126: * Router::share(array(array('GET', '/'), array('POST', '/')), 'home@index');
127: * </code>
128: *
129: * @param array $routes
130: * @param mixed $action
131: * @return void
132: */
133: public static function share($routes, $action)
134: {
135: foreach ($routes as $route)
136: {
137: static::register($route[0], $route[1], $action);
138: }
139: }
140:
141: /**
142: * Register a group of routes that share attributes.
143: *
144: * @param array $attributes
145: * @param Closure $callback
146: * @return void
147: */
148: public static function group($attributes, Closure $callback)
149: {
150: // Route groups allow the developer to specify attributes for a group
151: // of routes. To register them, we'll set a static property on the
152: // router so that the register method will see them.
153: static::$group = $attributes;
154:
155: call_user_func($callback);
156:
157: // Once the routes have been registered, we want to set the group to
158: // null so the attributes will not be given to any of the routes
159: // that are added after the group is declared.
160: static::$group = null;
161: }
162:
163: /**
164: * Register a route with the router.
165: *
166: * <code>
167: * // Register a route with the router
168: * Router::register('GET', '/', function() {return 'Home!';});
169: *
170: * // Register a route that handles multiple URIs with the router
171: * Router::register(array('GET', '/', 'GET /home'), function() {return 'Home!';});
172: * </code>
173: *
174: * @param string $method
175: * @param string|array $route
176: * @param mixed $action
177: * @return void
178: */
179: public static function register($method, $route, $action)
180: {
181: if (ctype_digit($route)) $route = "({$route})";
182:
183: if (is_string($route)) $route = explode(', ', $route);
184:
185: // If the developer is registering multiple request methods to handle
186: // the URI, we'll spin through each method and register the route
187: // for each of them along with each URI and action.
188: if (is_array($method))
189: {
190: foreach ($method as $http)
191: {
192: static::register($http, $route, $action);
193: }
194:
195: return;
196: }
197:
198: foreach ((array) $route as $uri)
199: {
200: // If the URI begins with a splat, we'll call the universal method, which
201: // will register a route for each of the request methods supported by
202: // the router. This is just a notational short-cut.
203: if ($method == '*')
204: {
205: foreach (static::$methods as $method)
206: {
207: static::register($method, $route, $action);
208: }
209:
210: continue;
211: }
212:
213: $uri = ltrim(str_replace('(:bundle)', static::$bundle, $uri), '/');
214:
215: if($uri == '')
216: {
217: $uri = '/';
218: }
219:
220: // If the URI begins with a wildcard, we want to add this route to the
221: // array of "fallback" routes. Fallback routes are always processed
222: // last when parsing routes since they are very generic and could
223: // overload bundle routes that are registered.
224: if ($uri[0] == '(')
225: {
226: $routes =& static::$fallback;
227: }
228: else
229: {
230: $routes =& static::$routes;
231: }
232:
233: // If the action is an array, we can simply add it to the array of
234: // routes keyed by the URI. Otherwise, we will need to call into
235: // the action method to get a valid action array.
236: if (is_array($action))
237: {
238: $routes[$method][$uri] = $action;
239: }
240: else
241: {
242: $routes[$method][$uri] = static::action($action);
243: }
244:
245: // If a group is being registered, we'll merge all of the group
246: // options into the action, giving preference to the action
247: // for options that are specified in both.
248: if ( ! is_null(static::$group))
249: {
250: $routes[$method][$uri] += static::$group;
251: }
252:
253: // If the HTTPS option is not set on the action, we'll use the
254: // value given to the method. The secure method passes in the
255: // HTTPS value in as a parameter short-cut.
256: if ( ! isset($routes[$method][$uri]['https']))
257: {
258: $routes[$method][$uri]['https'] = false;
259: }
260: }
261: }
262:
263: /**
264: * Convert a route action to a valid action array.
265: *
266: * @param mixed $action
267: * @return array
268: */
269: protected static function action($action)
270: {
271: // If the action is a string, it is a pointer to a controller, so we
272: // need to add it to the action array as a "uses" clause, which will
273: // indicate to the route to call the controller.
274: if (is_string($action))
275: {
276: $action = array('uses' => $action);
277: }
278: // If the action is a Closure, we will manually put it in an array
279: // to work around a bug in PHP 5.3.2 which causes Closures cast
280: // as arrays to become null. We'll remove this.
281: elseif ($action instanceof Closure)
282: {
283: $action = array($action);
284: }
285:
286: return (array) $action;
287: }
288:
289: /**
290: * Register a secure controller with the router.
291: *
292: * @param string|array $controllers
293: * @param string|array $defaults
294: * @return void
295: */
296: public static function secure_controller($controllers, $defaults = 'index')
297: {
298: static::controller($controllers, $defaults, true);
299: }
300:
301: /**
302: * Register a controller with the router.
303: *
304: * @param string|array $controllers
305: * @param string|array $defaults
306: * @param bool $https
307: * @return void
308: */
309: public static function controller($controllers, $defaults = 'index', $https = null)
310: {
311: foreach ((array) $controllers as $identifier)
312: {
313: list($bundle, $controller) = Bundle::parse($identifier);
314:
315: // First we need to replace the dots with slashes in the controller name
316: // so that it is in directory format. The dots allow the developer to use
317: // a cleaner syntax when specifying the controller. We will also grab the
318: // root URI for the controller's bundle.
319: $controller = str_replace('.', '/', $controller);
320:
321: $root = Bundle::option($bundle, 'handles');
322:
323: // If the controller is a "home" controller, we'll need to also build an
324: // index method route for the controller. We'll remove "home" from the
325: // route root and setup a route to point to the index method.
326: if (ends_with($controller, 'home'))
327: {
328: static::root($identifier, $controller, $root);
329: }
330:
331: // The number of method arguments allowed for a controller is set by a
332: // "segments" constant on this class which allows for the developer to
333: // increase or decrease the limit on method arguments.
334: $wildcards = static::repeat('(:any?)', static::$segments);
335:
336: // Once we have the path and root URI we can build a simple route for
337: // the controller that should handle a conventional controller route
338: // setup of controller/method/segment/segment, etc.
339: $pattern = trim("{$root}/{$controller}/{$wildcards}", '/');
340:
341: // Finally we can build the "uses" clause and the attributes for the
342: // controller route and register it with the router with a wildcard
343: // method so it is available on every request method.
344: $uses = "{$identifier}@(:1)";
345:
346: $attributes = compact('uses', 'defaults', 'https');
347:
348: static::register('*', $pattern, $attributes);
349: }
350: }
351:
352: /**
353: * Register a route for the root of a controller.
354: *
355: * @param string $identifier
356: * @param string $controller
357: * @param string $root
358: * @return void
359: */
360: protected static function root($identifier, $controller, $root)
361: {
362: // First we need to strip "home" off of the controller name to create the
363: // URI needed to match the controller's folder, which should match the
364: // root URI we want to point to the index method.
365: if ($controller !== 'home')
366: {
367: $home = dirname($controller);
368: }
369: else
370: {
371: $home = '';
372: }
373:
374: // After we trim the "home" off of the controller name we'll build the
375: // pattern needed to map to the controller and then register a route
376: // to point the pattern to the controller's index method.
377: $pattern = trim($root.'/'.$home, '/') ?: '/';
378:
379: $attributes = array('uses' => "{$identifier}@index");
380:
381: static::register('*', $pattern, $attributes);
382: }
383:
384: /**
385: * Find a route by the route's assigned name.
386: *
387: * @param string $name
388: * @return array
389: */
390: public static function find($name)
391: {
392: if (isset(static::$names[$name])) return static::$names[$name];
393:
394: // If no route names have been found at all, we will assume no reverse
395: // routing has been done, and we will load the routes file for all of
396: // the bundles that are installed for the application.
397: if (count(static::$names) == 0)
398: {
399: foreach (Bundle::names() as $bundle)
400: {
401: Bundle::routes($bundle);
402: }
403: }
404:
405: // To find a named route, we will iterate through every route defined
406: // for the application. We will cache the routes by name so we can
407: // load them very quickly the next time.
408: foreach (static::routes() as $method => $routes)
409: {
410: foreach ($routes as $key => $value)
411: {
412: if (isset($value['as']) and $value['as'] === $name)
413: {
414: return static::$names[$name] = array($key => $value);
415: }
416: }
417: }
418: }
419:
420: /**
421: * Find the route that uses the given action.
422: *
423: * @param string $action
424: * @return array
425: */
426: public static function uses($action)
427: {
428: // If the action has already been reverse routed before, we'll just
429: // grab the previously found route to save time. They are cached
430: // in a static array on the class.
431: if (isset(static::$uses[$action]))
432: {
433: return static::$uses[$action];
434: }
435:
436: Bundle::routes(Bundle::name($action));
437:
438: // To find the route, we'll simply spin through the routes looking
439: // for a route with a "uses" key matching the action, and if we
440: // find one, we cache and return it.
441: foreach (static::routes() as $method => $routes)
442: {
443: foreach ($routes as $key => $value)
444: {
445: if (isset($value['uses']) and $value['uses'] === $action)
446: {
447: return static::$uses[$action] = array($key => $value);
448: }
449: }
450: }
451: }
452:
453: /**
454: * Search the routes for the route matching a method and URI.
455: *
456: * @param string $method
457: * @param string $uri
458: * @return Route
459: */
460: public static function route($method, $uri)
461: {
462: Bundle::start($bundle = Bundle::handles($uri));
463:
464: $routes = (array) static::method($method);
465:
466: // Of course literal route matches are the quickest to find, so we will
467: // check for those first. If the destination key exists in the routes
468: // array we can just return that route now.
469: if (array_key_exists($uri, $routes))
470: {
471: $action = $routes[$uri];
472:
473: return new Route($method, $uri, $action);
474: }
475:
476: // If we can't find a literal match we'll iterate through all of the
477: // registered routes to find a matching route based on the route's
478: // regular expressions and wildcards.
479: if ( ! is_null($route = static::match($method, $uri)))
480: {
481: return $route;
482: }
483: }
484:
485: /**
486: * Iterate through every route to find a matching route.
487: *
488: * @param string $method
489: * @param string $uri
490: * @return Route
491: */
492: protected static function match($method, $uri)
493: {
494: foreach (static::method($method) as $route => $action)
495: {
496: // We only need to check routes with regular expression since all others
497: // would have been able to be matched by the search for literal matches
498: // we just did before we started searching.
499: if (str_contains($route, '('))
500: {
501: $pattern = '#^'.static::wildcards($route).'$#u';
502:
503: // If we get a match we'll return the route and slice off the first
504: // parameter match, as preg_match sets the first array item to the
505: // full-text match of the pattern.
506: if (preg_match($pattern, $uri, $parameters))
507: {
508: return new Route($method, $route, $action, array_slice($parameters, 1));
509: }
510: }
511: }
512: }
513:
514: /**
515: * Translate route URI wildcards into regular expressions.
516: *
517: * @param string $key
518: * @return string
519: */
520: protected static function wildcards($key)
521: {
522: list($search, $replace) = array_divide(static::$optional);
523:
524: // For optional parameters, first translate the wildcards to their
525: // regex equivalent, sans the ")?" ending. We'll add the endings
526: // back on when we know the replacement count.
527: $key = str_replace($search, $replace, $key, $count);
528:
529: if ($count > 0)
530: {
531: $key .= str_repeat(')?', $count);
532: }
533:
534: return strtr($key, static::$patterns);
535: }
536:
537: /**
538: * Get all of the registered routes, with fallbacks at the end.
539: *
540: * @return array
541: */
542: public static function routes()
543: {
544: $routes = static::$routes;
545:
546: foreach (static::$methods as $method)
547: {
548: // It's possible that the routes array may not contain any routes for the
549: // method, so we'll seed each request method with an empty array if it
550: // doesn't already contain any routes.
551: if ( ! isset($routes[$method])) $routes[$method] = array();
552:
553: $fallback = array_get(static::$fallback, $method, array());
554:
555: // When building the array of routes, we'll merge in all of the fallback
556: // routes for each request method individually. This allows us to avoid
557: // collisions when merging the arrays together.
558: $routes[$method] = array_merge($routes[$method], $fallback);
559: }
560:
561: return $routes;
562: }
563:
564: /**
565: * Grab all of the routes for a given request method.
566: *
567: * @param string $method
568: * @return array
569: */
570: public static function method($method)
571: {
572: $routes = array_get(static::$routes, $method, array());
573:
574: return array_merge($routes, array_get(static::$fallback, $method, array()));
575: }
576:
577: /**
578: * Get all of the wildcard patterns
579: *
580: * @return array
581: */
582: public static function patterns()
583: {
584: return array_merge(static::$patterns, static::$optional);
585: }
586:
587: /**
588: * Get a string repeating a URI pattern any number of times.
589: *
590: * @param string $pattern
591: * @param int $times
592: * @return string
593: */
594: protected static function repeat($pattern, $times)
595: {
596: return implode('/', array_fill(0, $times, $pattern));
597: }
598:
599: }
600: