Archive for the ‘php query string’ tag

Pretty PHP URLS without mod_rewrite

leave a comment

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:

  1. The first argument is the controller we goto (the module in Codon’s case)
  2. Arguments can vary based on the controller
  3. Have a default ruleset if we don’t specify for a module
  4. The ability to “dynamically” change the arguments on the fly, to accommodate different actions
  5. Also parse the traditional query string rules
  6. We can specify types for each argument, as a security measure.
  7. 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:

echo $_SERVER['REQUEST_URI'];

# Will output:
/index.php/testing/our/parameters

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:

Array
(
	[default] => Array
	(
		[action] => string
		[id] => int
	)

	[users] => Array
	(
		[action] => string
		[username] => string
	)
)

So if we look at default:

index.php/{action}/{id}

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)

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);
	}
}

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:

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'));

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:

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;
}

Some of it is pretty straight forward, but that’s half of the “workhorse”, next is the ProcessModuleRewrite() function:

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;
		}
	}
}

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:

CodonRewrite::AddRule("default", array('action'=>'string', 'id'=>'int'));
CodonRewrite::AddRule("users", array('action', 'username'));
CodonRewrite::ProcessRewrite();

echo CodonRewrite::$get->module;

What I do is for a controller, I’ll assign $this->get to CodonRewrite::$get, so inside a controller, I can do:

class MyModule extends CodonModule
{
	...

	function Controller()
	{

		if($this->get->action == ...)
		...
	}
}

And since rules can be “redone” on the fly:

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')
		...
	}
}

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!

Written by Nabeel

March 3rd, 2009 at 4:12 pm

Posted in php

Tagged with , , ,

Nginx + PHP Query Strings

2 comments

Nginx (Engine-X) is the sweetest web server I’ve used. It’s running my VPS now, CPU usage has barely budged, even with a decent number of visitors. I ran into a problem, which I was searching for a solution before I cracked at it myself. The solution ended up being incredibly simple, but still may help someone else who’s searchin’ for it.

With nginx, PHP is passed of to FastCGI PHP process to parse and execute. It relies on a location rule to find out what are PHP files. But it would fall apart using URLs like:

index.php/some/query/string/parameters

Apache doesn’t have any trouble, with it, but nginx, if you looked at the error log, it’ll be about the directory not existing. (Hmm, parsing that in PHP sounds like a good idea for another post… noted).

So if we check out the current rewrite rules:

location ~ \.php$ {
		include /etc/nginx/conf/fastcgi_params;
		fastcgi_pass  127.0.0.1:9000;
		fastcgi_index index.php;
		fastcgi_param SCRIPT_FILENAME /path/to/public/$fastcgi_script_name;
}

The fix is pretty simple:

location ~ \.php<strong>(.*)</strong>$ {
		include /etc/nginx/conf/fastcgi_params;
		fastcgi_pass  127.0.0.1:9000;
		fastcgi_index index.php;
		fastcgi_param SCRIPT_FILENAME /path/to/public/$fastcgi_script_name;
}

Adding in the (.*) after the .php allows the PHP files to be picked up, and those additional query string parameters will get passed along into FastCGI.

Simple fix!

Written by Nabeel

February 24th, 2009 at 6:30 pm

Posted in General

Tagged with , , ,