URI Routing overhaul
- Allow multiple levels of controller directories (supersedes PRs #390, #2439)
- Add support for per-directory 'defaul_controller' and '404_override' (resolves issue #2611; supersedes PR #939)
- Fixed a bug where default_controller was called instead of triggering 404 if the current route is inside a directory
- Removed a few calls from CI_Router to CI_URI that made a necessity for otherwise internal CI_URI methods to be public:
- Removed CI_URI::_fetch_uri_string() and moved its logic into CI_URI::__construct()
- Removed CI_URI::_remove_url_suffix, CI_URI::_explode_segments() and moved their logic into CI_URI::_set_uri_string()
- Removed CI_URI::_reindex_segments() altogether ( doesn't need further manipulation, while is
public anyway and can be properly (and more effectively) replaced on the spot)
diff --git a/system/core/Router.php b/system/core/Router.php
index 71530ff..e3c9115 100644
--- a/system/core/Router.php
+++ b/system/core/Router.php
@@ -91,6 +91,15 @@
*/
public $translate_uri_dashes = FALSE;
+ /**
+ * Enable query strings flag
+ *
+ * Determines wether to use GET parameters or segment URIs
+ *
+ * @var bool
+ */
+ public $enable_query_strings = FALSE;
+
// --------------------------------------------------------------------
/**
@@ -106,6 +115,8 @@
$this->config =& load_class('Config', 'core');
$this->uri =& load_class('URI', 'core');
+
+ $this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE);
$this->_set_routing();
// Set any routing overrides that may exist in the main index file
@@ -146,26 +157,39 @@
// Are query strings enabled in the config file? Normally CI doesn't utilize query strings
// since URI segments are more search-engine friendly, but they can optionally be used.
// If this feature is enabled, we will gather the directory/class/method a little differently
- $segments = array();
- if ($this->config->item('enable_query_strings') === TRUE
- && ! empty($_GET[$this->config->item('controller_trigger')])
- && is_string($_GET[$this->config->item('controller_trigger')])
- )
+ if ($this->enable_query_strings)
{
- if (isset($_GET[$this->config->item('directory_trigger')]) && is_string($_GET[$this->config->item('directory_trigger')]))
+ $_d = $this->config->item('directory_trigger');
+ $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';
+ if ($_d !== '')
{
- $this->set_directory(trim($this->uri->filter_uri($_GET[$this->config->item('directory_trigger')])));
- $segments[] = $this->directory;
+ $this->set_directory($this->uri->filter_uri($_d));
}
- $this->set_class(trim($this->uri->filter_uri($_GET[$this->config->item('controller_trigger')])));
- $segments[] = $this->class;
-
- if ( ! empty($_GET[$this->config->item('function_trigger')]) && is_string($_GET[$this->config->item('function_trigger')]))
+ $_c = $this->config->item('controller_trigger');
+ if ( ! empty($_GET[$_c]))
{
- $this->set_method(trim($this->uri->filter_uri($_GET[$this->config->item('function_trigger')])));
- $segments[] = $this->method;
+ $this->set_class(trim($this->uri->filter_uri(trim($_GET[$_c]))));
+
+ $_f = $this->config->item('function_trigger');
+ if ( ! empty($_GET[$_f]))
+ {
+ $this->set_method(trim($this->uri->filter_uri($_GET[$_f])));
+ }
+
+ $this->uri->rsegments = array(
+ 1 => $this->class,
+ 2 => $this->method
+ );
}
+ else
+ {
+ $this->_set_default_controller();
+ }
+
+ // Routing rules don't apply to query strings and we don't need to detect
+ // directories, so we're done here
+ return;
}
// Load the routes.php file.
@@ -188,25 +212,58 @@
$this->routes = $route;
}
- // Were there any query string segments? If so, we'll validate them and bail out since we're done.
- if (count($segments) > 0)
+ // Is there anything to parse?
+ if ($this->uri->uri_string !== '')
{
- return $this->_validate_request($segments);
+ $this->_parse_routes();
+ }
+ else
+ {
+ $this->_set_default_controller();
+ }
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Set request route
+ *
+ * Takes an array of URI segments as input and sets the class/method
+ * to be called.
+ *
+ * @used-by CI_Router::_parse_routes()
+ * @param array $segments URI segments
+ * @return void
+ */
+ protected function _set_request($segments = array())
+ {
+ $segments = $this->_validate_request($segments);
+ // If we don't have any segments left - try the default controller;
+ // WARNING: Directories get shifted out of the segments array!
+ if (empty($segments))
+ {
+ $this->_set_default_controller();
+ return;
}
- // Fetch the complete URI string
- $this->uri->_fetch_uri_string();
-
- // Is there a URI string? If not, the default controller specified in the "routes" file will be shown.
- if ($this->uri->uri_string == '')
+ if ($this->translate_uri_dashes === TRUE)
{
- return $this->_set_default_controller();
+ $segments[0] = str_replace('-', '_', $segments[0]);
+ if (isset($segments[1]))
+ {
+ $segments[1] = str_replace('-', '_', $segments[1]);
+ }
}
- $this->uri->_remove_url_suffix(); // Remove the URL suffix
- $this->uri->_explode_segments(); // Compile the segments into an array
- $this->_parse_routes(); // Parse any custom routing that may exist
- $this->uri->_reindex_segments(); // Re-index the segment array so that it starts with 1 rather than 0
+ $this->set_class($segments[0]);
+ if (isset($segments[1]))
+ {
+ $this->set_method($segments[1]);
+ }
+
+ array_unshift($segments, NULL);
+ unset($segments[0]);
+ $this->uri->rsegments = $segments;
}
// --------------------------------------------------------------------
@@ -229,10 +286,20 @@
$method = 'index';
}
- $this->_set_request(array($class, $method));
+ if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php'))
+ {
+ // This will trigger 404 later
+ return;
+ }
- // re-index the routed segments array so it starts with 1 rather than 0
- $this->uri->_reindex_segments();
+ $this->set_class($class);
+ $this->set_method($method);
+
+ // Assign routed segments, index starting from 1
+ $this->uri->rsegments = array(
+ 1 => $class,
+ 2 => $method
+ );
log_message('debug', 'No URI present. Default controller set.');
}
@@ -240,117 +307,35 @@
// --------------------------------------------------------------------
/**
- * Set request route
- *
- * Takes an array of URI segments as input and sets the class/method
- * to be called.
- *
- * @param array $segments URI segments
- * @return void
- */
- protected function _set_request($segments = array())
- {
- $segments = $this->_validate_request($segments);
-
- if (count($segments) === 0)
- {
- return $this->_set_default_controller();
- }
-
- if ($this->translate_uri_dashes === TRUE)
- {
- $segments[0] = str_replace('-', '_', $segments[0]);
- if (isset($segments[1]))
- {
- $segments[1] = str_replace('-', '_', $segments[1]);
- }
- }
-
- $this->set_class($segments[0]);
- isset($segments[1]) OR $segments[1] = 'index';
- $this->set_method($segments[1]);
-
- // Update our "routed" segment array to contain the segments.
- // Note: If there is no custom routing, this array will be
- // identical to $this->uri->segments
- $this->uri->rsegments = $segments;
- }
-
- // --------------------------------------------------------------------
-
- /**
* Validate request
*
* Attempts validate the URI request and determine the controller path.
*
+ * @used-by CI_Router::_set_request()
* @param array $segments URI segments
- * @return array URI segments
+ * @return mixed URI segments
*/
protected function _validate_request($segments)
{
- if (count($segments) === 0)
+ $c = count($segments);
+ // Loop through our segments and return as soon as a controller
+ // is found or when such a directory doesn't exist
+ while ($c-- > 0)
{
- return $segments;
- }
+ $test = $this->directory
+ .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
- $test = ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
-
- // Does the requested controller exist in the root folder?
- if (file_exists(APPPATH.'controllers/'.$test.'.php'))
- {
- return $segments;
- }
-
- // Is the controller in a sub-folder?
- if (is_dir(APPPATH.'controllers/'.$segments[0]))
- {
- // Set the directory and remove it from the segment array
- $this->set_directory(array_shift($segments));
- if (count($segments) > 0)
+ if ( ! file_exists(APPPATH.'controllers/'.$test.'.php') && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0]))
{
- $test = ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
-
- // Does the requested controller exist in the sub-directory?
- if ( ! file_exists(APPPATH.'controllers/'.$this->directory.$test.'.php'))
- {
- if ( ! empty($this->routes['404_override']))
- {
- $this->directory = '';
- return explode('/', $this->routes['404_override'], 2);
- }
- else
- {
- show_404($this->directory.$segments[0]);
- }
- }
- }
- else
- {
- // Is the method being specified in the route?
- $segments = explode('/', $this->default_controller);
- if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($segments[0]).'.php'))
- {
- $this->directory = '';
- }
+ $this->set_directory(array_shift($segments), TRUE);
+ continue;
}
return $segments;
}
- // If we've gotten this far it means that the URI does not correlate to a valid
- // controller class. We will now see if there is an override
- if ( ! empty($this->routes['404_override']))
- {
- if (sscanf($this->routes['404_override'], '%[^/]/%s', $class, $method) !== 2)
- {
- $method = 'index';
- }
-
- return array($class, $method);
- }
-
- // Nothing else to do at this point but show a 404
- show_404($segments[0]);
+ // This means that all segments were actually directories
+ return $segments;
}
// --------------------------------------------------------------------
@@ -377,12 +362,14 @@
// Check default routes format
if (is_string($this->routes[$uri]))
{
- return $this->_set_request(explode('/', $this->routes[$uri]));
+ $this->_set_request(explode('/', $this->routes[$uri]));
+ return;
}
// Is there a matching http verb?
elseif (is_array($this->routes[$uri]) && isset($this->routes[$uri][$http_verb]))
{
- return $this->_set_request(explode('/', $this->routes[$uri][$http_verb]));
+ $this->_set_request(explode('/', $this->routes[$uri][$http_verb]));
+ return;
}
}
@@ -452,7 +439,8 @@
$val = preg_replace('#^'.$key.'$#', $val, $uri);
}
- return $this->_set_request(explode('/', $val));
+ $this->_set_request(explode('/', $val));
+ return;
}
}
@@ -519,11 +507,19 @@
* Set directory name
*
* @param string $dir Directory name
+ * @param bool $appent Whether we're appending rather then setting the full value
* @return void
*/
- public function set_directory($dir)
+ public function set_directory($dir, $append = FALSE)
{
- $this->directory = str_replace(array('/', '.'), '', $dir).'/';
+ if ($append !== TRUE OR empty($this->directory))
+ {
+ $this->directory = str_replace('.', '', trim($dir, '/')).'/';
+ }
+ else
+ {
+ $this->directory .= str_replace('.', '', trim($dir, '/')).'/';
+ }
}
// --------------------------------------------------------------------