router.php 15 KB

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