Route.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. <?php defined('SYSPATH') OR die('No direct script access.');
  2. /**
  3. * Routes are used to determine the controller and action for a requested URI.
  4. * Every route generates a regular expression which is used to match a URI
  5. * and a route. Routes may also contain keys which can be used to set the
  6. * controller, action, and parameters.
  7. *
  8. * Each <key> will be translated to a regular expression using a default
  9. * regular expression pattern. You can override the default pattern by providing
  10. * a pattern for the key:
  11. *
  12. * // This route will only match when <id> is a digit
  13. * Route::set('user', 'user/<action>/<id>', array('id' => '\d+'));
  14. *
  15. * // This route will match when <path> is anything
  16. * Route::set('file', '<path>', array('path' => '.*'));
  17. *
  18. * It is also possible to create optional segments by using parentheses in
  19. * the URI definition:
  20. *
  21. * // This is the standard default route, and no keys are required
  22. * Route::set('default', '(<controller>(/<action>(/<id>)))');
  23. *
  24. * // This route only requires the <file> key
  25. * Route::set('file', '(<path>/)<file>(.<format>)', array('path' => '.*', 'format' => '\w+'));
  26. *
  27. * Routes also provide a way to generate URIs (called "reverse routing"), which
  28. * makes them an extremely powerful and flexible way to generate internal links.
  29. *
  30. * @package Kohana
  31. * @category Base
  32. * @author Kohana Team
  33. * @copyright (c) 2008-2012 Kohana Team
  34. * @license http://kohanaframework.org/license
  35. */
  36. class Kohana_Route {
  37. // Defines the pattern of a <segment>
  38. const REGEX_KEY = '<([a-zA-Z0-9_]++)>';
  39. // What can be part of a <segment> value
  40. const REGEX_SEGMENT = '[^/.,;?\n]++';
  41. // What must be escaped in the route regex
  42. const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';
  43. /**
  44. * @var string default protocol for all routes
  45. *
  46. * @example 'http://'
  47. */
  48. public static $default_protocol = 'http://';
  49. /**
  50. * @var array list of valid localhost entries
  51. */
  52. public static $localhosts = array(FALSE, '', 'local', 'localhost');
  53. /**
  54. * @var string default action for all routes
  55. */
  56. public static $default_action = 'index';
  57. /**
  58. * @var bool Indicates whether routes are cached
  59. */
  60. public static $cache = FALSE;
  61. /**
  62. * @var array
  63. */
  64. protected static $_routes = array();
  65. /**
  66. * Stores a named route and returns it. The "action" will always be set to
  67. * "index" if it is not defined.
  68. *
  69. * Route::set('default', '(<controller>(/<action>(/<id>)))')
  70. * ->defaults(array(
  71. * 'controller' => 'welcome',
  72. * ));
  73. *
  74. * @param string $name route name
  75. * @param string $uri URI pattern
  76. * @param array $regex regex patterns for route keys
  77. * @return Route
  78. */
  79. public static function set($name, $uri = NULL, $regex = NULL)
  80. {
  81. return Route::$_routes[$name] = new Route($uri, $regex);
  82. }
  83. /**
  84. * Retrieves a named route.
  85. *
  86. * $route = Route::get('default');
  87. *
  88. * @param string $name route name
  89. * @return Route
  90. * @throws Kohana_Exception
  91. */
  92. public static function get($name)
  93. {
  94. if ( ! isset(Route::$_routes[$name]))
  95. {
  96. throw new Kohana_Exception('The requested route does not exist: :route',
  97. array(':route' => $name));
  98. }
  99. return Route::$_routes[$name];
  100. }
  101. /**
  102. * Retrieves all named routes.
  103. *
  104. * $routes = Route::all();
  105. *
  106. * @return array routes by name
  107. */
  108. public static function all()
  109. {
  110. return Route::$_routes;
  111. }
  112. /**
  113. * Get the name of a route.
  114. *
  115. * $name = Route::name($route)
  116. *
  117. * @param Route $route instance
  118. * @return string
  119. */
  120. public static function name(Route $route)
  121. {
  122. return array_search($route, Route::$_routes);
  123. }
  124. /**
  125. * Saves or loads the route cache. If your routes will remain the same for
  126. * a long period of time, use this to reload the routes from the cache
  127. * rather than redefining them on every page load.
  128. *
  129. * if ( ! Route::cache())
  130. * {
  131. * // Set routes here
  132. * Route::cache(TRUE);
  133. * }
  134. *
  135. * @param boolean $save cache the current routes
  136. * @param boolean $append append, rather than replace, cached routes when loading
  137. * @return void when saving routes
  138. * @return boolean when loading routes
  139. * @uses Kohana::cache
  140. */
  141. public static function cache($save = FALSE, $append = FALSE)
  142. {
  143. if ($save === TRUE)
  144. {
  145. try
  146. {
  147. // Cache all defined routes
  148. Kohana::cache('Route::cache()', Route::$_routes);
  149. }
  150. catch (Exception $e)
  151. {
  152. // We most likely have a lambda in a route, which cannot be cached
  153. throw new Kohana_Exception('One or more routes could not be cached (:message)', array(
  154. ':message' => $e->getMessage(),
  155. ), 0, $e);
  156. }
  157. }
  158. else
  159. {
  160. if ($routes = Kohana::cache('Route::cache()'))
  161. {
  162. if ($append)
  163. {
  164. // Append cached routes
  165. Route::$_routes += $routes;
  166. }
  167. else
  168. {
  169. // Replace existing routes
  170. Route::$_routes = $routes;
  171. }
  172. // Routes were cached
  173. return Route::$cache = TRUE;
  174. }
  175. else
  176. {
  177. // Routes were not cached
  178. return Route::$cache = FALSE;
  179. }
  180. }
  181. }
  182. /**
  183. * Create a URL from a route name. This is a shortcut for:
  184. *
  185. * echo URL::site(Route::get($name)->uri($params), $protocol);
  186. *
  187. * @param string $name route name
  188. * @param array $params URI parameters
  189. * @param mixed $protocol protocol string or boolean, adds protocol and domain
  190. * @return string
  191. * @since 3.0.7
  192. * @uses URL::site
  193. */
  194. public static function url($name, array $params = NULL, $protocol = NULL)
  195. {
  196. $route = Route::get($name);
  197. // Create a URI with the route and convert it to a URL
  198. if ($route->is_external())
  199. return Route::get($name)->uri($params);
  200. else
  201. return URL::site(Route::get($name)->uri($params), $protocol);
  202. }
  203. /**
  204. * Returns the compiled regular expression for the route. This translates
  205. * keys and optional groups to a proper PCRE regular expression.
  206. *
  207. * $compiled = Route::compile(
  208. * '<controller>(/<action>(/<id>))',
  209. * array(
  210. * 'controller' => '[a-z]+',
  211. * 'id' => '\d+',
  212. * )
  213. * );
  214. *
  215. * @return string
  216. * @uses Route::REGEX_ESCAPE
  217. * @uses Route::REGEX_SEGMENT
  218. */
  219. public static function compile($uri, array $regex = NULL)
  220. {
  221. // The URI should be considered literal except for keys and optional parts
  222. // Escape everything preg_quote would escape except for : ( ) < >
  223. $expression = preg_replace('#'.Route::REGEX_ESCAPE.'#', '\\\\$0', $uri);
  224. if (strpos($expression, '(') !== FALSE)
  225. {
  226. // Make optional parts of the URI non-capturing and optional
  227. $expression = str_replace(array('(', ')'), array('(?:', ')?'), $expression);
  228. }
  229. // Insert default regex for keys
  230. $expression = str_replace(array('<', '>'), array('(?P<', '>'.Route::REGEX_SEGMENT.')'), $expression);
  231. if ($regex)
  232. {
  233. $search = $replace = array();
  234. foreach ($regex as $key => $value)
  235. {
  236. $search[] = "<$key>".Route::REGEX_SEGMENT;
  237. $replace[] = "<$key>$value";
  238. }
  239. // Replace the default regex with the user-specified regex
  240. $expression = str_replace($search, $replace, $expression);
  241. }
  242. return '#^'.$expression.'$#uD';
  243. }
  244. /**
  245. * @var array route filters
  246. */
  247. protected $_filters = array();
  248. /**
  249. * @var string route URI
  250. */
  251. protected $_uri = '';
  252. /**
  253. * @var array
  254. */
  255. protected $_regex = array();
  256. /**
  257. * @var array
  258. */
  259. protected $_defaults = array('action' => 'index', 'host' => FALSE);
  260. /**
  261. * @var string
  262. */
  263. protected $_route_regex;
  264. /**
  265. * Creates a new route. Sets the URI and regular expressions for keys.
  266. * Routes should always be created with [Route::set] or they will not
  267. * be properly stored.
  268. *
  269. * $route = new Route($uri, $regex);
  270. *
  271. * The $uri parameter should be a string for basic regex matching.
  272. *
  273. *
  274. * @param string $uri route URI pattern
  275. * @param array $regex key patterns
  276. * @return void
  277. * @uses Route::_compile
  278. */
  279. public function __construct($uri = NULL, $regex = NULL)
  280. {
  281. if ($uri === NULL)
  282. {
  283. // Assume the route is from cache
  284. return;
  285. }
  286. if ( ! empty($uri))
  287. {
  288. $this->_uri = $uri;
  289. }
  290. if ( ! empty($regex))
  291. {
  292. $this->_regex = $regex;
  293. }
  294. // Store the compiled regex locally
  295. $this->_route_regex = Route::compile($uri, $regex);
  296. }
  297. /**
  298. * Provides default values for keys when they are not present. The default
  299. * action will always be "index" unless it is overloaded here.
  300. *
  301. * $route->defaults(array(
  302. * 'controller' => 'welcome',
  303. * 'action' => 'index'
  304. * ));
  305. *
  306. * If no parameter is passed, this method will act as a getter.
  307. *
  308. * @param array $defaults key values
  309. * @return $this or array
  310. */
  311. public function defaults(array $defaults = NULL)
  312. {
  313. if ($defaults === NULL)
  314. {
  315. return $this->_defaults;
  316. }
  317. $this->_defaults = $defaults;
  318. return $this;
  319. }
  320. /**
  321. * Filters to be run before route parameters are returned:
  322. *
  323. * $route->filter(
  324. * function(Route $route, $params, Request $request)
  325. * {
  326. * if ($request->method() !== HTTP_Request::POST)
  327. * {
  328. * return FALSE; // This route only matches POST requests
  329. * }
  330. * if ($params AND $params['controller'] === 'welcome')
  331. * {
  332. * $params['controller'] = 'home';
  333. * }
  334. *
  335. * return $params;
  336. * }
  337. * );
  338. *
  339. * To prevent a route from matching, return `FALSE`. To replace the route
  340. * parameters, return an array.
  341. *
  342. * [!!] Default parameters are added before filters are called!
  343. *
  344. * @throws Kohana_Exception
  345. * @param array $callback callback string, array, or closure
  346. * @return $this
  347. */
  348. public function filter($callback)
  349. {
  350. if ( ! is_callable($callback))
  351. {
  352. throw new Kohana_Exception('Invalid Route::callback specified');
  353. }
  354. $this->_filters[] = $callback;
  355. return $this;
  356. }
  357. /**
  358. * Tests if the route matches a given URI. A successful match will return
  359. * all of the routed parameters as an array. A failed match will return
  360. * boolean FALSE.
  361. *
  362. * // Params: controller = users, action = edit, id = 10
  363. * $params = $route->matches('users/edit/10');
  364. *
  365. * This method should almost always be used within an if/else block:
  366. *
  367. * if ($params = $route->matches($uri))
  368. * {
  369. * // Parse the parameters
  370. * }
  371. *
  372. * @param string $uri URI to match
  373. * @return array on success
  374. * @return FALSE on failure
  375. */
  376. public function matches(Request $request)
  377. {
  378. // Get the URI from the Request
  379. $uri = trim($request->uri(), '/');
  380. if ( ! preg_match($this->_route_regex, $uri, $matches))
  381. return FALSE;
  382. $params = array();
  383. foreach ($matches as $key => $value)
  384. {
  385. if (is_int($key))
  386. {
  387. // Skip all unnamed keys
  388. continue;
  389. }
  390. // Set the value for all matched keys
  391. $params[$key] = $value;
  392. }
  393. foreach ($this->_defaults as $key => $value)
  394. {
  395. if ( ! isset($params[$key]) OR $params[$key] === '')
  396. {
  397. // Set default values for any key that was not matched
  398. $params[$key] = $value;
  399. }
  400. }
  401. if ( ! empty($params['controller']))
  402. {
  403. // PSR-0: Replace underscores with spaces, run ucwords, then replace underscore
  404. $params['controller'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $params['controller'])));
  405. }
  406. if ( ! empty($params['directory']))
  407. {
  408. // PSR-0: Replace underscores with spaces, run ucwords, then replace underscore
  409. $params['directory'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $params['directory'])));
  410. }
  411. if ($this->_filters)
  412. {
  413. foreach ($this->_filters as $callback)
  414. {
  415. // Execute the filter giving it the route, params, and request
  416. $return = call_user_func($callback, $this, $params, $request);
  417. if ($return === FALSE)
  418. {
  419. // Filter has aborted the match
  420. return FALSE;
  421. }
  422. elseif (is_array($return))
  423. {
  424. // Filter has modified the parameters
  425. $params = $return;
  426. }
  427. }
  428. }
  429. return $params;
  430. }
  431. /**
  432. * Returns whether this route is an external route
  433. * to a remote controller.
  434. *
  435. * @return boolean
  436. */
  437. public function is_external()
  438. {
  439. return ! in_array(Arr::get($this->_defaults, 'host', FALSE), Route::$localhosts);
  440. }
  441. /**
  442. * Generates a URI for the current route based on the parameters given.
  443. *
  444. * // Using the "default" route: "users/profile/10"
  445. * $route->uri(array(
  446. * 'controller' => 'users',
  447. * 'action' => 'profile',
  448. * 'id' => '10'
  449. * ));
  450. *
  451. * @param array $params URI parameters
  452. * @return string
  453. * @throws Kohana_Exception
  454. * @uses Route::REGEX_Key
  455. */
  456. public function uri(array $params = NULL)
  457. {
  458. // Start with the routed URI
  459. $uri = $this->_uri;
  460. if (strpos($uri, '<') === FALSE AND strpos($uri, '(') === FALSE)
  461. {
  462. // This is a static route, no need to replace anything
  463. if ( ! $this->is_external())
  464. return $uri;
  465. // If the localhost setting does not have a protocol
  466. if (strpos($this->_defaults['host'], '://') === FALSE)
  467. {
  468. // Use the default defined protocol
  469. $params['host'] = Route::$default_protocol.$this->_defaults['host'];
  470. }
  471. else
  472. {
  473. // Use the supplied host with protocol
  474. $params['host'] = $this->_defaults['host'];
  475. }
  476. // Compile the final uri and return it
  477. return rtrim($params['host'], '/').'/'.$uri;
  478. }
  479. // Keep track of whether an optional param was replaced
  480. $provided_optional = FALSE;
  481. while (preg_match('#\([^()]++\)#', $uri, $match))
  482. {
  483. // Search for the matched value
  484. $search = $match[0];
  485. // Remove the parenthesis from the match as the replace
  486. $replace = substr($match[0], 1, -1);
  487. while (preg_match('#'.Route::REGEX_KEY.'#', $replace, $match))
  488. {
  489. list($key, $param) = $match;
  490. if (isset($params[$param]) AND $params[$param] !== Arr::get($this->_defaults, $param))
  491. {
  492. // Future optional params should be required
  493. $provided_optional = TRUE;
  494. // Replace the key with the parameter value
  495. $replace = str_replace($key, $params[$param], $replace);
  496. }
  497. elseif ($provided_optional)
  498. {
  499. // Look for a default
  500. if (isset($this->_defaults[$param]))
  501. {
  502. $replace = str_replace($key, $this->_defaults[$param], $replace);
  503. }
  504. else
  505. {
  506. // Ungrouped parameters are required
  507. throw new Kohana_Exception('Required route parameter not passed: :param', array(
  508. ':param' => $param,
  509. ));
  510. }
  511. }
  512. else
  513. {
  514. // This group has missing parameters
  515. $replace = '';
  516. break;
  517. }
  518. }
  519. // Replace the group in the URI
  520. $uri = str_replace($search, $replace, $uri);
  521. }
  522. while (preg_match('#'.Route::REGEX_KEY.'#', $uri, $match))
  523. {
  524. list($key, $param) = $match;
  525. if ( ! isset($params[$param]))
  526. {
  527. // Look for a default
  528. if (isset($this->_defaults[$param]))
  529. {
  530. $params[$param] = $this->_defaults[$param];
  531. }
  532. else
  533. {
  534. // Ungrouped parameters are required
  535. throw new Kohana_Exception('Required route parameter not passed: :param', array(
  536. ':param' => $param,
  537. ));
  538. }
  539. }
  540. $uri = str_replace($key, $params[$param], $uri);
  541. }
  542. // Trim all extra slashes from the URI
  543. $uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
  544. if ($this->is_external())
  545. {
  546. // Need to add the host to the URI
  547. $host = $this->_defaults['host'];
  548. if (strpos($host, '://') === FALSE)
  549. {
  550. // Use the default defined protocol
  551. $host = Route::$default_protocol.$host;
  552. }
  553. // Clean up the host and prepend it to the URI
  554. $uri = rtrim($host, '/').'/'.$uri;
  555. }
  556. return $uri;
  557. }
  558. } // End Route