Added Route::$cache flag, fixes #3212. Includes tests.
[kohana-core.git] / classes / kohana / route.php
blobdb37e6fc9a396a6b9231701a58e04f3c900810c8
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.
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:
12 * // This route will only match when <id> is a digit
13 * Route::set('user', 'user/<action>/<id>', array('id' => '\d+'));
15 * // This route will match when <path> is anything
16 * Route::set('file', '<path>', array('path' => '.*'));
18 * It is also possible to create optional segments by using parentheses in
19 * the URI definition:
21 * // This is the standard default route, and no keys are required
22 * Route::set('default', '(<controller>(/<action>(/<id>)))');
24 * // This route only requires the <file> key
25 * Route::set('file', '(<path>/)<file>(.<format>)', array('path' => '.*', 'format' => '\w+'));
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.
30 * @package Kohana
31 * @category Base
32 * @author Kohana Team
33 * @copyright (c) 2008-2010 Kohana Team
34 * @license http://kohanaframework.org/license
36 class Kohana_Route {
38 // Defines the pattern of a <segment>
39 const REGEX_KEY = '<([a-zA-Z0-9_]++)>';
41 // What can be part of a <segment> value
42 const REGEX_SEGMENT = '[^/.,;?\n]++';
44 // What must be escaped in the route regex
45 const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';
47 /**
48 * @var string default protocol for all routes
50 * @example 'http://'
52 public static $default_protocol = 'http://';
54 /**
55 * @var array list of valid localhost entries
57 public static $localhosts = array(FALSE, '', 'local', 'localhost');
59 /**
60 * @var string default action for all routes
62 public static $default_action = 'index';
64 /**
65 * @var bool Indicates whether routes are cached
67 public static $cache = FALSE;
69 // List of route objects
70 protected static $_routes = array();
72 /**
73 * Stores a named route and returns it. The "action" will always be set to
74 * "index" if it is not defined.
76 * Route::set('default', '(<controller>(/<action>(/<id>)))')
77 * ->defaults(array(
78 * 'controller' => 'welcome',
79 * ));
81 * @param string route name
82 * @param string URI pattern
83 * @param array regex patterns for route keys
84 * @return Route
86 public static function set($name, $uri, array $regex = NULL)
88 return Route::$_routes[$name] = new Route($uri, $regex);
91 /**
92 * Retrieves a named route.
94 * $route = Route::get('default');
96 * @param string route name
97 * @return Route
98 * @throws Kohana_Exception
100 public static function get($name)
102 if ( ! isset(Route::$_routes[$name]))
104 throw new Kohana_Exception('The requested route does not exist: :route',
105 array(':route' => $name));
108 return Route::$_routes[$name];
112 * Retrieves all named routes.
114 * $routes = Route::all();
116 * @return array routes by name
118 public static function all()
120 return Route::$_routes;
124 * Get the name of a route.
126 * $name = Route::name($route)
128 * @param object Route instance
129 * @return string
131 public static function name(Route $route)
133 return array_search($route, Route::$_routes);
137 * Saves or loads the route cache. If your routes will remain the same for
138 * a long period of time, use this to reload the routes from the cache
139 * rather than redefining them on every page load.
141 * if ( ! Route::cache())
143 * // Set routes here
144 * Route::cache(TRUE);
147 * @param boolean cache the current routes
148 * @return void when saving routes
149 * @return boolean when loading routes
150 * @uses Kohana::cache
152 public static function cache($save = FALSE)
154 if ($save === TRUE)
156 // Cache all defined routes
157 Kohana::cache('Route::cache()', Route::$_routes);
159 else
161 if ($routes = Kohana::cache('Route::cache()'))
163 Route::$_routes = $routes;
165 // Routes were cached
166 return Route::$cache = TRUE;
168 else
170 // Routes were not cached
171 return Route::$cache = FALSE;
177 * Create a URL from a route name. This is a shortcut for:
179 * echo URL::site(Route::get($name)->uri($params), $protocol);
181 * @param string route name
182 * @param array URI parameters
183 * @param mixed protocol string or boolean, adds protocol and domain
184 * @return string
185 * @since 3.0.7
186 * @uses URL::site
188 public static function url($name, array $params = NULL, $protocol = NULL)
190 // Create a URI with the route and convert it to a URL
191 return URL::site(Route::get($name)->uri($params), $protocol);
195 * Returns the compiled regular expression for the route. This translates
196 * keys and optional groups to a proper PCRE regular expression.
198 * $compiled = Route::compile(
199 * '<controller>(/<action>(/<id>))',
200 * array(
201 * 'controller' => '[a-z]+',
202 * 'id' => '\d+',
204 * );
206 * @return string
207 * @uses Route::REGEX_ESCAPE
208 * @uses Route::REGEX_SEGMENT
210 public static function compile($uri, array $regex = NULL)
212 // The URI should be considered literal except for keys and optional parts
213 // Escape everything preg_quote would escape except for : ( ) < >
214 $expression = preg_replace('#'.Route::REGEX_ESCAPE.'#', '\\\\$0', $uri);
216 if (strpos($expression, '(') !== FALSE)
218 // Make optional parts of the URI non-capturing and optional
219 $expression = str_replace(array('(', ')'), array('(?:', ')?'), $expression);
222 // Insert default regex for keys
223 $expression = str_replace(array('<', '>'), array('(?P<', '>'.Route::REGEX_SEGMENT.')'), $expression);
225 if ($regex)
227 $search = $replace = array();
228 foreach ($regex as $key => $value)
230 $search[] = "<$key>".Route::REGEX_SEGMENT;
231 $replace[] = "<$key>$value";
234 // Replace the default regex with the user-specified regex
235 $expression = str_replace($search, $replace, $expression);
238 return '#^'.$expression.'$#uD';
242 * @var string route URI
244 protected $_uri = '';
246 // Regular expressions for route keys
247 protected $_regex = array();
249 // Default values for route keys
250 protected $_defaults = array('action' => 'index');
252 // Compiled regex cache
253 protected $_route_regex;
256 * Creates a new route. Sets the URI and regular expressions for keys.
257 * Routes should always be created with [Route::set] or they will not
258 * be properly stored.
260 * $route = new Route($uri, $regex);
262 * @param string route URI pattern
263 * @param array key patterns
264 * @return void
265 * @uses Route::_compile
267 public function __construct($uri = NULL, array $regex = NULL)
269 if ($uri === NULL)
271 // Assume the route is from cache
272 return;
275 if ( ! empty($regex))
277 $this->_regex = $regex;
280 // Store the URI that this route will match
281 $this->_uri = $uri;
283 // Store the compiled regex locally
284 $this->_route_regex = Route::compile($uri, $regex);
288 * Provides default values for keys when they are not present. The default
289 * action will always be "index" unless it is overloaded here.
291 * $route->defaults(array(
292 * 'controller' => 'welcome',
293 * 'action' => 'index'
294 * ));
296 * @param array key values
297 * @return $this
299 public function defaults(array $defaults = NULL)
301 $this->_defaults = $defaults;
303 return $this;
307 * Tests if the route matches a given URI. A successful match will return
308 * all of the routed parameters as an array. A failed match will return
309 * boolean FALSE.
311 * // Params: controller = users, action = edit, id = 10
312 * $params = $route->matches('users/edit/10');
314 * This method should almost always be used within an if/else block:
316 * if ($params = $route->matches($uri))
318 * // Parse the parameters
321 * @param string URI to match
322 * @return array on success
323 * @return FALSE on failure
325 public function matches($uri)
327 if ( ! preg_match($this->_route_regex, $uri, $matches))
328 return FALSE;
330 $params = array();
331 foreach ($matches as $key => $value)
333 if (is_int($key))
335 // Skip all unnamed keys
336 continue;
339 // Set the value for all matched keys
340 $params[$key] = $value;
343 foreach ($this->_defaults as $key => $value)
345 if ( ! isset($params[$key]) OR $params[$key] === '')
347 // Set default values for any key that was not matched
348 $params[$key] = $value;
352 return $params;
356 * Generates a URI for the current route based on the parameters given.
358 * // Using the "default" route: "users/profile/10"
359 * $route->uri(array(
360 * 'controller' => 'users',
361 * 'action' => 'profile',
362 * 'id' => '10'
363 * ));
365 * @param array URI parameters
366 * @return string
367 * @throws Kohana_Exception
368 * @uses Route::REGEX_Key
370 public function uri(array $params = NULL)
372 if ($params === NULL)
374 // Use the default parameters
375 $params = $this->_defaults;
377 else
379 // Add the default parameters
380 $params += $this->_defaults;
383 // Start with the routed URI
384 $uri = $this->_uri;
386 if (strpos($uri, '<') === FALSE AND strpos($uri, '(') === FALSE)
388 // This is a static route, no need to replace anything
389 return $uri;
392 while (preg_match('#\([^()]++\)#', $uri, $match))
394 // Search for the matched value
395 $search = $match[0];
397 // Remove the parenthesis from the match as the replace
398 $replace = substr($match[0], 1, -1);
400 while (preg_match('#'.Route::REGEX_KEY.'#', $replace, $match))
402 list($key, $param) = $match;
404 if ($param === 'host')
405 continue;
407 if (isset($params[$param]))
409 // Replace the key with the parameter value
410 $replace = str_replace($key, $params[$param], $replace);
412 else
414 // This group has missing parameters
415 $replace = '';
416 break;
420 // Replace the group in the URI
421 $uri = str_replace($search, $replace, $uri);
424 while (preg_match('#'.Route::REGEX_KEY.'#', $uri, $match))
426 list($key, $param) = $match;
428 if ( ! isset($params[$param]))
430 // Ungrouped parameters are required
431 throw new Kohana_Exception('Required route parameter not passed: :param', array(
432 ':param' => $param,
436 $uri = str_replace($key, $params[$param], $uri);
439 // Trim all extra slashes from the URI
440 $uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
442 // If the localhost setting matches a local route, return the uri as is
443 if ( ! isset($params['host']) OR in_array($params['host'], Route::$localhosts))
444 return $uri;
446 // If the localhost setting does not have a protocol
447 if (strpos($params['host'], '://') === FALSE)
449 // Use the default defined protocol
450 $params['host'] = Route::$default_protocol.$params['host'];
453 // Compile the final uri and return it
454 return rtrim($params['host'], '/').'/'.$uri;
457 } // End Route