Pretty PHP URLS without mod_rewrite
Not everyone has access to mod_rewrite, or doing rewrite rules through nginx or lighttpd, but in PHP, you can do this pretty simply. I’ll go through making this mess:
index.php?module=amodule&page=tasklist&id=10
To:
index.php/amodule/tasklist/10
I’ll be going through the code from Codon, specifically the rewrite module, which can handle this. So a few requirements:
- The first argument is the controller we goto (the module in Codon’s case)
- Arguments can vary based on the controller
- Have a default ruleset if we don’t specify for a module
- The ability to “dynamically” change the arguments on the fly, to accommodate different actions
- Also parse the traditional query string rules
- We can specify types for each argument, as a security measure.
- Use a static class, and have the parameters available through $_GET, as well as from the class
PHP stores the query string in the $_SERVER['REQUEST_URI'] variable as:
[php]
echo $_SERVER['REQUEST_URI'];
# Will output:
/index.php/testing/our/parameters
[/php]
Being relative to the base URL. We’ll work on parsing this later. First we need to build our list of “rewrite rules”. What we’re going to do is store the rules in an associative array, based on our module, and each key has an array, with the rules in order, and we specify the types in that. What’d he say? This will explain it best:
[code]
Array
(
[default] => Array
(
[action] => string
[id] => int
)
[users] => Array
(
[action] => string
[username] => string
)
)
[/code]
So if we look at default:
[code]
index.php/{action}/{id}
[/code]
Where action and ID will be treated as strings. So we’ll build a function for that (Note: This is all in a static class)
[php]
public static function AddRule($module, $params)
{
$set_params=array();
$module = strtolower($module);
# Format the rules, make sure we arrange
foreach($params as $key=>$value)
{
# If it wasn’t done as $key=>$value, just $key,
# set the default value type as a string
if(is_numeric($key))
$set_params[$value]=’string’;
else
$set_params[$key]=$value;
}
self::$rewrite_rules[$module] = $set_params;
# This is for if we’ve already processed the rules
# once. This will allow the rules to be changed
# "on the fly", for example, inside a controller
if(self::$run == true)
{
# Reprocess the rules
self::ProcessModuleRewrite($module);
}
}
[/php]
There is the self::$run “clause”, which sees whether the rules have been parsed or not, and if so, then we’ll just process that particular module. This is to then if the rules are changed on the fly, after we reprocess, then the entire procedure doesn’t have to be done. This will make more sense later.
So we go through, and set the proper types for the parameters.
We call this as:
[php]
CodonRewrite::AddRule(‘default’, array(‘action’=>’string’, ‘id’=>’int’));
# If we do it as keys, it’ll be strings as default, as we’ll see later
CodonRewrite::AddRule(‘users’, array(‘action’, ‘username’));
[/php]
Now we can actually parse the URL, which is the bit we’re interested in. I use explode()’s to parse out the pieces, as I find that to be the most legible:
[php]
public static function ProcessRewrite()
{
$URL = $_SERVER['REQUEST_URI'];
# Get everything after the .php/ and before the ?
$params = explode(‘.php/’, $URL);
$preg_match = $params[1];
$params = explode(‘?’, $preg_match);
$split_parameters = $params[0];
# Now check if there’s anything there (we didn’t just have
# index.php?query_string=…
# If that’s all, then we grab a configuration setting that
# specifies the default rewrite, ie: news/showall
# Which would eq. passing index.php/news/showall
if($split_parameters == ”)
{
$split_parameters = Config::Get(‘DEFAULT_MODULE’);
}
# Now we split it all out, and store the peices
self::$peices = explode(‘/’, $split_parameters);
$module_name = strtolower(self::$peices[0]);
if($module_name == ”) # If it’s blank, check $_GET
{
$module_name = $_GET['module'];
}
self::$current_module = $module_name;
$_GET['module'] = $module_name;
# Create the object to hold all of our stuff
self::$get = new stdClass;
# If we haven’t specified specific rules for a module,
# Then we use the rules we made for "default"
if(!array_key_exists($module_name, self::$rewrite_rules))
{
$module_name = ‘default’;
}
# This parses now the rules for a specific module
self::ProcessModuleRewrite($module_name);
# And this tacks on our $_GET rules
parse_str($_SERVER['QUERY_STRING'], $get_extra);
$_GET = array_merge($_GET, $get_extra);
# Add the $_GET to our object
foreach($_GET as $key=>$value)
{
self::$get->$key = $value;
}
self::$run = true;
}
[/php]
Some of it is pretty straight forward, but that’s half of the “workhorse”, next is the ProcessModuleRewrite() function:
[php]
public static function ProcessModuleRewrite($module_name)
{
# Make sure it’s valid
if(is_array(self::$rewrite_rules[$module_name]))
{
# Walk through every peice of the array, $key is the
# index name, and $type is well, the type
$i=1;
foreach(self::$rewrite_rules[$module_name] as $key=>$type)
{
# Each peice, which was saved above as the exploded URL
$val = self::$peices[$i++];
# Convert to type specified
if($type == ‘int’)
$val = intval($val);
elseif($type == ‘float’)
$val = floatval($val);
# We can do any other processing we want here
# Add it both into the $_GET array, and into
# our object
self::$get->$key = $val;
$_GET[$key] = $val;
}
}
}
[/php]
So as we see, this is where the types come into play, and we do our conversions. We can also strip any slashes here, and whatever other stuff. So now we can do:
[php]
CodonRewrite::AddRule("default", array(‘action’=>’string’, ‘id’=>’int’));
CodonRewrite::AddRule("users", array(‘action’, ‘username’));
CodonRewrite::ProcessRewrite();
echo CodonRewrite::$get->module;
[/php]
What I do is for a controller, I’ll assign $this->get to CodonRewrite::$get, so inside a controller, I can do:
[php]
class MyModule extends CodonModule
{
…
function Controller()
{
if($this->get->action == …)
…
}
}
[/php]
And since rules can be “redone” on the fly:
[php]
class MyModule extends CodonModule
{
…
function __construct()
{
# Since I see it’s a save, I’m modifying the rules a bit here
# so I can include another extra parameter in
if($this->get->action == ‘save’)
{
CodonRewrite::AddRule(‘mymodule’, array(‘page’, ‘action’, ‘id’=>’int’, ‘content’));
}
…
}
function Controller()
{
if($this->get->action == ‘save’)
…
}
}
[/php]
I call this the Rewrite functions as the first thing in my main controller, and then I can base which module/controller to call based on these rules here. Make’s it simple for parsing, and pretty easy to see how the rules are made.
I hope that helps someone!