PageRenderTime 24ms CodeModel.GetById 41ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/kohana/route.php

https://github.com/ymtn/core
PHP | 589 lines | 234 code | 70 blank | 285 comment | 27 complexity | a7350df10bea5f8a3dc28d8cb9219bd9 MD5 | raw file
  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-2011 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 route name
  75. * @param string URI pattern
  76. * @param array regex patterns for route keys
  77. * @return Route
  78. */
  79. public static function set($name, $uri_callback = NULL, $regex = NULL)
  80. {
  81. return Route::$_routes[$name] = new Route($uri_callback, $regex);
  82. }
  83. /**
  84. * Retrieves a named route.
  85. *
  86. * $route = Route::get('default');
  87. *
  88. * @param string 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 object 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 cache the current routes
  136. * @return void when saving routes
  137. * @return boolean when loading routes
  138. * @uses Kohana::cache
  139. */
  140. public static function cache($save = FALSE)
  141. {
  142. if ($save === TRUE)
  143. {
  144. // Cache all defined routes
  145. Kohana::cache('Route::cache()', Route::$_routes);
  146. }
  147. else
  148. {
  149. if ($routes = Kohana::cache('Route::cache()'))
  150. {
  151. Route::$_routes = $routes;
  152. // Routes were cached
  153. return Route::$cache = TRUE;
  154. }
  155. else
  156. {
  157. // Routes were not cached
  158. return Route::$cache = FALSE;
  159. }
  160. }
  161. }
  162. /**
  163. * Create a URL from a route name. This is a shortcut for:
  164. *
  165. * echo URL::site(Route::get($name)->uri($params), $protocol);
  166. *
  167. * @param string route name
  168. * @param array URI parameters
  169. * @param mixed protocol string or boolean, adds protocol and domain
  170. * @return string
  171. * @since 3.0.7
  172. * @uses URL::site
  173. */
  174. public static function url($name, array $params = NULL, $protocol = NULL)
  175. {
  176. $route = Route::get($name);
  177. // Create a URI with the route and convert it to a URL
  178. if ($route->is_external())
  179. return Route::get($name)->uri($params);
  180. else
  181. return URL::site(Route::get($name)->uri($params), $protocol);
  182. }
  183. /**
  184. * Returns the compiled regular expression for the route. This translates
  185. * keys and optional groups to a proper PCRE regular expression.
  186. *
  187. * $compiled = Route::compile(
  188. * '<controller>(/<action>(/<id>))',
  189. * array(
  190. * 'controller' => '[a-z]+',
  191. * 'id' => '\d+',
  192. * )
  193. * );
  194. *
  195. * @return string
  196. * @uses Route::REGEX_ESCAPE
  197. * @uses Route::REGEX_SEGMENT
  198. */
  199. public static function compile($uri, array $regex = NULL)
  200. {
  201. if ( ! is_string($uri))
  202. return;
  203. // The URI should be considered literal except for keys and optional parts
  204. // Escape everything preg_quote would escape except for : ( ) < >
  205. $expression = preg_replace('#'.Route::REGEX_ESCAPE.'#', '\\\\$0', $uri);
  206. if (strpos($expression, '(') !== FALSE)
  207. {
  208. // Make optional parts of the URI non-capturing and optional
  209. $expression = str_replace(array('(', ')'), array('(?:', ')?'), $expression);
  210. }
  211. // Insert default regex for keys
  212. $expression = str_replace(array('<', '>'), array('(?P<', '>'.Route::REGEX_SEGMENT.')'), $expression);
  213. if ($regex)
  214. {
  215. $search = $replace = array();
  216. foreach ($regex as $key => $value)
  217. {
  218. $search[] = "<$key>".Route::REGEX_SEGMENT;
  219. $replace[] = "<$key>$value";
  220. }
  221. // Replace the default regex with the user-specified regex
  222. $expression = str_replace($search, $replace, $expression);
  223. }
  224. return '#^'.$expression.'$#uD';
  225. }
  226. /**
  227. * @var callback The callback method for routes
  228. */
  229. protected $_callback;
  230. /**
  231. * @var string route URI
  232. */
  233. protected $_uri = '';
  234. /**
  235. * @var array
  236. */
  237. protected $_regex = array();
  238. /**
  239. * @var array
  240. */
  241. protected $_defaults = array('action' => 'index', 'host' => FALSE);
  242. /**
  243. * @var array
  244. */
  245. protected $_retain=array();
  246. /**
  247. * @var string
  248. */
  249. protected $_route_regex;
  250. /**
  251. * Creates a new route. Sets the URI and regular expressions for keys.
  252. * Routes should always be created with [Route::set] or they will not
  253. * be properly stored.
  254. *
  255. * $route = new Route($uri, $regex);
  256. *
  257. * The $uri parameter can either be a string for basic regex matching or it
  258. * can be a valid callback or anonymous function (php 5.3+). If you use a
  259. * callback or anonymous function, your method should return an array
  260. * containing the proper keys for the route. If you want the route to be
  261. * "reversable", you need pass the route string as the third parameter.
  262. *
  263. * $route = new Route(function($uri)
  264. * {
  265. * if (list($controller, $action, $param) = explode('/', $uri) AND $controller == 'foo' AND $action == 'bar')
  266. * {
  267. * return array(
  268. * 'controller' => 'foobar',
  269. * 'action' => $action,
  270. * 'id' => $param,
  271. * );
  272. * },
  273. * 'foo/bar/<id>'
  274. * });
  275. *
  276. * @param mixed route URI pattern or lambda/callback function
  277. * @param array key patterns
  278. * @return void
  279. * @uses Route::_compile
  280. */
  281. public function __construct($uri = NULL, $regex = NULL)
  282. {
  283. if ($uri === NULL)
  284. {
  285. // Assume the route is from cache
  286. return;
  287. }
  288. if ( ! is_string($uri) AND is_callable($uri))
  289. {
  290. $this->_callback = $uri;
  291. $this->_uri = $regex;
  292. $regex = NULL;
  293. }
  294. elseif ( ! empty($uri))
  295. {
  296. $this->_uri = $uri;
  297. }
  298. if ( ! empty($regex))
  299. {
  300. $this->_regex = $regex;
  301. }
  302. // Store the compiled regex locally
  303. $this->_route_regex = Route::compile($uri, $regex);
  304. }
  305. /**
  306. * Provides default values for keys when they are not present. The default
  307. * action will always be "index" unless it is overloaded here.
  308. *
  309. * $route->defaults(array(
  310. * 'controller' => 'welcome',
  311. * 'action' => 'index'
  312. * ));
  313. *
  314. * @param array key values
  315. * @return $this
  316. */
  317. public function defaults(array $defaults = NULL)
  318. {
  319. $this->_defaults = $defaults;
  320. return $this;
  321. }
  322. /**
  323. * Provides default values for optional keys in a route
  324. * when using Route::uri
  325. *
  326. * @param array $params
  327. * @return $this
  328. */
  329. public function uri_defaults(array $params = NULL)
  330. {
  331. $this->_uri_defaults = $params;
  332. return $this;
  333. }
  334. /**
  335. * When matching a url to a route in Route::matches,
  336. * the keys in _retain will be added to _uri_defaults
  337. *
  338. *
  339. * @param array $keys
  340. * @return $this
  341. */
  342. public function retain(array $keys = NULL)
  343. {
  344. $this->_retain = $keys;
  345. return $this;
  346. }
  347. /**
  348. * Tests if the route matches a given URI. A successful match will return
  349. * all of the routed parameters as an array. A failed match will return
  350. * boolean FALSE.
  351. *
  352. * // Params: controller = users, action = edit, id = 10
  353. * $params = $route->matches('users/edit/10');
  354. *
  355. * This method should almost always be used within an if/else block:
  356. *
  357. * if ($params = $route->matches($uri))
  358. * {
  359. * // Parse the parameters
  360. * }
  361. *
  362. * @param string URI to match
  363. * @return array on success
  364. * @return FALSE on failure
  365. */
  366. public function matches($uri)
  367. {
  368. if ($this->_callback)
  369. {
  370. $closure = $this->_callback;
  371. $params = call_user_func($closure, $uri);
  372. if ( ! is_array($params))
  373. return FALSE;
  374. }
  375. else
  376. {
  377. if ( ! preg_match($this->_route_regex, $uri, $matches))
  378. return FALSE;
  379. $params = array();
  380. foreach ($matches as $key => $value)
  381. {
  382. if (is_int($key))
  383. {
  384. // Skip all unnamed keys
  385. continue;
  386. }
  387. // Set the value for all matched keys
  388. $params[$key] = $value;
  389. if(is_array($this->_retain) AND in_array($key,$this->_retain))
  390. {
  391. // Value should be retained; add it to the _uri_defaults
  392. $this->_uri_defaults[$key] = $value;
  393. }
  394. }
  395. }
  396. foreach ($this->_defaults as $key => $value)
  397. {
  398. if ( ! isset($params[$key]) OR $params[$key] === '')
  399. {
  400. // Set default values for any key that was not matched
  401. $params[$key] = $value;
  402. }
  403. }
  404. return $params;
  405. }
  406. /**
  407. * Returns whether this route is an external route
  408. * to a remote controller.
  409. *
  410. * @return boolean
  411. */
  412. public function is_external()
  413. {
  414. return ! in_array(Arr::get($this->_defaults, 'host', FALSE), Route::$localhosts);
  415. }
  416. /**
  417. * Generates a URI for the current route based on the parameters given.
  418. *
  419. * // Using the "default" route: "users/profile/10"
  420. * $route->uri(array(
  421. * 'controller' => 'users',
  422. * 'action' => 'profile',
  423. * 'id' => '10'
  424. * ));
  425. *
  426. * @param array URI parameters
  427. * @return string
  428. * @throws Kohana_Exception
  429. * @uses Route::REGEX_Key
  430. */
  431. public function uri(array $params = NULL)
  432. {
  433. // Start with the routed URI
  434. $uri = $this->_uri;
  435. if (strpos($uri, '<') === FALSE AND strpos($uri, '(') === FALSE)
  436. {
  437. // This is a static route, no need to replace anything
  438. if ( ! $this->is_external())
  439. return $uri;
  440. // If the localhost setting does not have a protocol
  441. if (strpos($this->_defaults['host'], '://') === FALSE)
  442. {
  443. // Use the default defined protocol
  444. $params['host'] = Route::$default_protocol.$this->_defaults['host'];
  445. }
  446. else
  447. {
  448. // Use the supplied host with protocol
  449. $params['host'] = $this->_defaults['host'];
  450. }
  451. // Compile the final uri and return it
  452. return rtrim($params['host'], '/').'/'.$uri;
  453. }
  454. while (preg_match('#\([^()]++\)#', $uri, $match))
  455. {
  456. // Search for the matched value
  457. $search = $match[0];
  458. // Remove the parenthesis from the match as the replace
  459. $replace = substr($match[0], 1, -1);
  460. while (preg_match('#'.Route::REGEX_KEY.'#', $replace, $match))
  461. {
  462. list($key, $param) = $match;
  463. if (isset($params[$param]))
  464. {
  465. // Replace the key with the parameter value
  466. $replace = str_replace($key, $params[$param], $replace);
  467. }
  468. elseif(isset($this->_uri_defaults[$param]))
  469. {
  470. // Replace the key with a default parameter if set in _uri_defaults
  471. $replace = str_replace($key, $this->_uri_defaults[$param], $replace);
  472. }
  473. else
  474. {
  475. // This group has missing parameters
  476. $replace = '';
  477. break;
  478. }
  479. }
  480. // Replace the group in the URI
  481. $uri = str_replace($search, $replace, $uri);
  482. }
  483. while (preg_match('#'.Route::REGEX_KEY.'#', $uri, $match))
  484. {
  485. list($key, $param) = $match;
  486. if ( ! isset($params[$param]))
  487. {
  488. // Look for a default
  489. if (isset($this->_defaults[$param]))
  490. {
  491. $params[$param] = $this->_defaults[$param];
  492. }
  493. else
  494. {
  495. // Ungrouped parameters are required
  496. throw new Kohana_Exception('Required route parameter not passed: :param', array(
  497. ':param' => $param,
  498. ));
  499. }
  500. }
  501. $uri = str_replace($key, $params[$param], $uri);
  502. }
  503. // Trim all extra slashes from the URI
  504. $uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
  505. if ($this->is_external())
  506. {
  507. // Need to add the host to the URI
  508. $host = $this->_defaults['host'];
  509. if (strpos($host, '://') === FALSE)
  510. {
  511. // Use the default defined protocol
  512. $host = Route::$default_protocol.$host;
  513. }
  514. // Clean up the host and prepend it to the URI
  515. $uri = rtrim($host, '/').'/'.$uri;
  516. }
  517. return $uri;
  518. }
  519. } // End Route