Building Browsergames: Reducing Repetition (PHP)

One of the things that will slow you down and bore you the most about working on your own browsergame project is re-writing the exact same code. Unfortunately, that’s what we’ve been doing with our stats code – as reader sepp helpfully pointed out.

There’s a concept in the programming world known as ‘DRY’ – which stands for “Don’t Repeat Yourself”. What that means is that you should only ever write code to do something once – and if there are only slight variations between one piece of the code and another, you should merge them until the only differences are the ones that have to exist. By adhering to DRY principles, we can drastically cut down our development time, and make sure that we have less code overall to maintain(always a plus). Today, we’ll be using the concept of DRY to trim down our stats code.

So far, we’ve written three mostly-the-same-but-different pieces of stats code – one for item stats, one for monster stats, and one for user stats. The code for these pieces of logic are mostly the same – all that’s different between them is a few table and column names. Here’s what one of our getStat functions looks like:

function getStat($statName,$userID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	createIfNotExists($statName,$userID);
	$query = sprintf("SELECT value FROM user_stats WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND user_id = '%s'",
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($userID));
	$result = mysql_query($query);
	list($value) = mysql_fetch_row($result);
	return $value;		
}

While that’s fine for a single use, we’re now using almost exactly the same code in three different places – and three different files. We’re going to apply the DRY principle, and rewrite the code slightly so that any of our stats code can call it:

function getStatDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("SELECT value FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($value) = mysql_fetch_row($result);
	return $value;		
}

And with that, we can now use that single getStat function from any of our stats code – all we need to do is pass in the table and column names, in addition to which stats we want to retrieve.

Refactoring the code in this way works because our stat tables are all set up in the same way – one column has values, one column has the stat ID, and one column has the ID of whatever we’re tracking the stat for. If our tables were too varied, this might not work as easily.

If you look at the line where we create our $query variable using sprintf, you’ll notice that there are no quotes around the token for the table name – this is because MySQL won’t allow you to use quotes around your table name in select statements. You need to be aware that this has the potential to grow into a SQL injection vulnerability – so make sure that you aren’t using user input for your table name.

With that change made, let’s also make the change in our other functions:

function setStatDRY($tableName,$columnName,$statName,$trackingID,$value) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("UPDATE %s SET value = '%s' WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($value),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
}
 
function createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	$query = sprintf("SELECT count(value) FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($count) = mysql_fetch_row($result);
	if($count == 0) {
		// the stat doesn't exist; insert it into the database
		$query = sprintf("INSERT INTO %s(stat_id,%s,value) VALUES ((SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s'),'%s','%s')",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($trackingID),
		'0');
		mysql_query($query);
	}	
}

As you can see, it isn’t that big of a change – we just add the $tableName and $columnName arguments to our functions, and then modify the way that our queries are created.

You might be wondering why these functions have been showing up with the word ‘DRY’ after them – and that’s because I’m about to show you one of the cooler aspects of doing this. Now that we’ve created these versions of our functions, we can save them as stats-dry.php, and then modify our current stats code to work off of them. Here’s an example of what the new stats.php looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
 
require_once 'stats-dry.php';
define('TABLE','user_stats');
define('COLUMN','user_id');
 
function getStat($statName,$trackingID) {
	return getStatDRY(TABLE,COLUMN,$statName,$trackingID);
}
function setStat($statName,$trackingID,$value) {
	setStatDRY(TABLE,COLUMN,$statName,$trackingID,$value);
}
 
?>

And that’s where you start to see the real gains of DRY – now, all of our stats logic is inside stats-dry.php. We can go to our other code and change the way that our functions work, so that they call the DRY versions – without having to change any of the other code on the pages that are using it(like the index page, healer, forest, etc.). While our codebase is still relatively small and this could be accomplished relatively easily, this technique is valid for big or small codebases – it just gives you the flexibility to be able to do whatever you need to. And if your codebase is small enough that you feel like making the switch to only using the DRY code, you can remove the three extra stat libraries entirely.

One new thing that you may not understand with this code is the calls to define(). We are using define() to set up constants for our code – that way, if we ever decide that user stats should be stored inside user_stats_table instead of user_stats, or we decide to change the column name to something different, we only need to change it in the one place – another benefit of DRY.

Here’s what the finished code for stats-dry.php looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
 
function getStatDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("SELECT value FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($value) = mysql_fetch_row($result);
	return $value;		
}
 
function setStatDRY($tableName,$columnName,$statName,$trackingID,$value) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("UPDATE %s SET value = '%s' WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($value),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
}
 
function createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	$query = sprintf("SELECT count(value) FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($count) = mysql_fetch_row($result);
	if($count == 0) {
		// the stat doesn't exist; insert it into the database
		$query = sprintf("INSERT INTO %s(stat_id,%s,value) VALUES ((SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s'),'%s','%s')",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($trackingID),
		'0');
		mysql_query($query);
	}	
}
 
?>

And here’s what the new code inside stats.php, which was used to retrieve our user’s stats, looks like. You will need to modify the other stats code yourself – although it shouldn’t be too difficult, and you could even just swap all the code to using the DRY functions instead(as long as you’re sure your database structure won’t change):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
 
require_once 'stats-dry.php';
define('TABLE','user_stats');
define('COLUMN','user_id');
 
function getStat($statName,$trackingID) {
	return getStatDRY(TABLE,COLUMN,$statName,$trackingID);
}
function setStat($statName,$trackingID,$value) {
	setStatDRY(TABLE,COLUMN,$statName,$trackingID,$value);
}
 
?>

Wish there was more?

I'm considering writing an ebook - click here.

.

Luke is the primary editor of Building Browsergames, and has written a large portion of the articles that you read here. He generally has no idea what to say when asked to write about himself in the third person.

Monday, July 7th, 2008 Uncategorized
  • Patrick

    I did the same on the monster_stats and items as well as stats.php. Then when I try to go fight a monster this error comes up: Fatal error: Cannot redeclare getStat() (previously declared in H:\XAMPP\xampp\htdocs\stats.php:7) in H:\XAMPP\xampp\htdocs\monster_stats.php on line 9.
    If you could show me what the other two codes are supposed to look like that would be very helpful. Thank you.

  • Thanks for the tip, sepp - I'll have to look into that as well(it looks like this will be the week of re-designing for DRY =P)

  • Hey, that's great you considered my notes. I'm honored =) .

    Actually if you try to keep your code dry you could also expand this principle to cover your database design, too. Why not only have one stats table instead of three or more? Simply call one column "type" again (with ENUM) to choose which stats you want to select, add, delete or update. I haven't read the whole code absolutely attentively, but I ran over it ;) . The tables seem to be pretty exactly the same.

    Greetings from FEtMab
    Sepp

blog comments powered by Disqus

About

Building Browsergames is a blog about browsergames(also known as PBBG's). It's geared towards the beginner to intermediate developer who has an interest in building their own browsergame.

Sponsors

Got Something to Say?

Send an e-mail to luke@buildingbrowsergames.com, or get in touch through Twitter at http://twitter.com/bbrowsergames