CakePHP Models – multiple columns to the same table

This one took me a few to figure out. On VACentral, there are schedules, which have an arrival and departure point. These points are all stored in one table, so one row in schedule refers to multiple entries in the airports table. It looks something like (ok, not something like, but exactly):

Schedules:
id | departure_icao | arrival_icao

Airports
id | icao

So two ICAO columns in routes map to one same column in airports. The ICAO is a unique 4 character identifier, which is assigned to an airport. It’s quite simple actually, but took me a while to figure it it. First the Airports model:

class Airport extends AppModel
{
	public $name = 'Airport';
	public $primaryKey = 'id';
	public $actAs = array('Containable');
}

And then our Schedules model:

class Schedule extends AppModel
{
	public $name = 'Schedule';
	public $primaryKey = 'id';
	public $actsAs = array('Containable');	

	public $belongsTo = array(
		'DepartureAirport' => array(
			'className' => 'Airport',
			'foreignKey' => false,
			'conditions' => 'DepartureAirport.icao = Schedule.departure_icao',
			'fields' => '',
			'order' => ''
		),

		'ArrivalAirport' => array(
			'className' => 'Airport',
			'foreignKey' => false,
			'conditions' => 'ArrivalAirport.icao = Schedule.arrival_icao',
			'fields' => '',
			'order' => ''
		)
	);
}

So we used the $belongTo relationship, and we will define two relationships – “DepartureAirport” and “ArrivalAirport”. We also select the class we will use (which IMO, should really be called “modelName” or “useModel”, that really tripped me up, but I digress). Next, we define the conditions – we’ll use the relationship name (DepartureAirport or ArrivalAirport), and the column name, along with the column name on the current table it should join on. And that’s pretty much it. You don’t really need a relationship on the “receiving” end (the Airports table), unless you will be querying airports, and finding out what schedules go there. I’ll leave that upto you ;)

And then for the query itself:

$this->Schedule->contain('DepartureAirport', 'ArrivalAirport');
$schedule = $this->Schedule->find('first');

// Our Airport specific data will be contained in:
$schedule['DepartureAirport']
$schedule['ArrivalAirport']

Which will now return something like (etc fields ommitted):

Array
(
    [Schedule] => Array
        (
            [schedule_id] => 4178
            [airline_id] => 2
            [code] => AEA
            [flightnum] => 6371
            [depicao] => CYUL
            [arricao] => KJFK
        )

    [DepartureAirport] => Array
        (
            [airport_id] => 597
            [iata] => YUL
            [icao] => CYUL
            [name] => Montreal / Pierre Elliot Trudeau International Airport, Quebec
            [timezone] => US/Eastern
            [location] => Montreal QC Canada
            [lat] => 45.470556
            [lng] => -73.740833
        )

    [ArrivalAirport] => Array
        (
            [airport_id] => 268
            [iata] => JFK
            [icao] => KJFK
            [name] => JFK Airport
            [timezone] => US/Eastern
            [location] => New York-Kennedy NY
            [lat] => 40.6398262
            [lng] => -73.7787443
        )

)

Note how it's using the Containable behavior; this is so it doesn't pull every relationship you've defined with that table (the schedules table above has many more relationships, but for brevity, I only pulled the relevant ones). Not specifying Containable() is REALLY expensive, especially when you don't need all those relationships to be included in every time! To speed it up even more, you should specify the actual field names to pull (the SQL * operator is expensive).

ezDB and PHP 5.3

As-per Justin’s request, I’ve renamed by fork of ezSQL to ezDB. I’ve updated the github links, it’s now:

http://github.com/nshahzad/ezdb/

This will include changes to the class names, to keep it all even (ezSQL to ezDB). I’m working on APC caching right now, since that’s what I’m using on current project.

PHP 5.3 was also released today! This is an exciting release – with the addition of namespaces and __callStatic(), the static DB class will be much easier to work with (instead of replicating every function). I will be finishing up the 5.2 release first, and then subsequent releases and features, I think I will be posting to the PHP 5.3 release only, unless there’s demand to back-port it all.

I’ll also be implementing some features from CakePHP’s ORM, such as “findBy{ColumnName}({tablename})”, and other simple lookups. I’ve been using Cake alot too, and it’s a great framework.
Cheers!

ezSQL updates

Well, got github working great. Did an update today, including:

  • $allowed_columns parameter for quick_insert() and quick_update(). So you can pass in $_POST or $_GET, as well as a list of valid indices to use
  • (optional) Exception handling with try/catch (more info)
  • Set return type (object, associative array, numeric array) globally through $default_type

Also, I’m updating the wiki slowly, transferring all the useful information onto there, since it’s easy to keep everything together.

Thanks!

ezSQL on github

Learning how to use github, so I created a repo for ezSQL.

http://github.com/nshahzad/ezdb

I plan on creating another version (a fork I guess?) for PHP 5.3 when it hopefully launches next week. It’ll have proper namespace support. Haven’t gotten to memcache just yet (hey, been busy! :) , but at least there’s an easier route of distribution now.

Backups are important!

A huge flight sim site was hacked and destroyed this weekend  – avsim.com. An important lesson on why off-site backups are critical! They had two servers, and had a backup of A on B, and B on A. Both were taken out.

Ouch. A shame to see it go. I’ve been going there for 6/7 years now. Good luck to them in getting it back up… they had one of the largest file libraries around.

Been a long time…

Sheesh! Things have been real busy, haven’t had much time to sit down and come up with a coherent post. My next update to phpVMS keeps getting pushed back; it’s been crunch time at work.

Though I’ve been messing with a few things – I cut memory usage by about 50% in phpVMS by using __autoload(). Perhaps that can be a good next post.

But I guess the big news (maybe), is that I’m discontinuing Codon. Not that it affects anybody really except my private projects and the few projects it was used on, but I’d rather concentrate my efforts on my new love – CodeIgniter. I’m really digging it, it’s almost as it’s an exact copy of Codon, with how the models work, etc. Though there are alot of PHP4-isms, I think it’s better for me to focus some efforts on learning and modding that. Codon was a great learning project for MVC (which is what it was always meant as). I don’t think there’s any better way to learn how something works than to build it by scratch. You really get to see the ins-and-outs, the pros and cons and the dilemmas faced by doing something like that.

I’ll be porting my date/time math module over to CI soon, but also some mods I’ve been working on to introduce autoload() into CI (drastic memory improvements), and also maybe implement ezSQL into it, since ezSQL is much more lightweight than their ActiveRecord class. I’m using CI now as part of two broader projects involving phpVMS; right now it’s being used as the code behind the remote API which all the phpVMS installs do talk to. I definately like to squeeze any bit of optimizations I can find.

PHP Benchmarking

I came across a few sites which have some valuable information on the “cost” of certain functions or practices in PHP. It comes in handy to keep in mind and avoid doing, and also to remember that there are better ways of doing things. If your application is coming down to relying on micro-optimization, your application itself needs to be looked at. But there’s no harm in knowing what methods take more time than others. Or of course, you can use ‘em to impress a prospective employer ;)

There are also some lectures and talks on optimizing:

Some good stuff there to look over and to keep in mind, especially obvious things to avoid, such as including the count() inside every for() loop. Actually any of the stuff on http://talks.php.net are a good read, and good to see where PHP has evolved from and where it’s evolving to, and to understand how to leverage the language.

To benchmark your scripts, you can use the PEAR Benchmark package; I have PEAR in my path, so for me it was as simple as including the same code that was in the examples.

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:

  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:

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!

ezSQL (more stuff)

I think I’m going to setup a SVN repository for ezSQL, since I’ve got a few ideas. I’m going to integrate memcache as one of the caching options, in addition to the flat-file caching it does now. Any other suggestions?

Nginx + PHP Query Strings

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(.*)$ {
        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!