02 Mar 2010

Simple stats collection with CodeIgniter hooks

CodeIgniter 3 Comments

Yesterday, I launched the alpha version of Project Trackr, a web based project management system I’m working on. As well as the feedback I would receive from the users themselves, I wanted some additional data. I wanted to track the users journey throughout the site and to get some quantitative data on on the usage of the application. With this information, I would know which features are popular are which features aren’t being used, and I could then try to find out if they’re not being used because they’re not useful, their implementation is poor or people just don’t know about it.

But surely I could find out this sort of stuff from something like Google analytics? Not really, the URLs are unique to each project, task, milestone, file etc. I also wanted the raw numerical data so that I could query it however I wanted.
My immediate thought was to log the current activity at the start of each function in my controllers, but this would be time consuming, and as a developer, we always want the simplest solution for the job. So I thought about how this could be automated.

Hooks

Seeing as I want this code to execute for every request, then surely a hook is the best solution. You could also put the following code inside the constructor of a super controller, which I tend to prefer over using hooks, but in this case I felt hooks where the best solution, as the code executed in them isn’t directly related to the execution of the controller, so I’d rather keep the 2 separated. You can read more about hooks in the user guide.

Enough talk, show me the code

Ok, so first up, we need to enable hooks, which are disabled by default. This is simply a case of changing a value in the configuration file. Open up system/application/config/config.php and find the line

$config['enable_hooks'] = FALSE;

and replace it with

$config['enable_hooks'] = TRUE;

Next up, we need to define our hook point. As stated in the documentation for hooks, there are 8 different hook points available. The hook point you choose will affect the results you get. If you use a pre system hook, you won’t have the necessary libraries loaded, if you use something list a post system hook, then it will never get executed if you have a redirect in your code. This was very important to me, as I use redirects after all insert operations, so I wouldn’t actually log any new data being entered if I used the posst system hook. In the end, I went for the post controller constructor hook. The code for the hook looks like the following, and should go in system/application/config/hooks.php

$hook['post_controller_constructor'] = array(
	'class'    => 'Statistics',
	'function' => 'log_activity',
	'filename' => 'Statistics.php',
	'filepath' => 'hooks'
);

You may change the items within the array if you wish, but remember to use your values for the rest of this.

Next, we need to decide what we actually want to track. In my case, I wanted the following:

  • Account, project and user ID. These allow me to query events at a user, project and account level
  • Section – The part of the app the request occurs in, this is the controller
  • Action – What the user is doing, this is the method in the controller
  • When – A timestamp of when this occured, so I can query across date rannges
  • URI – The URI string, as it may contain other useful information, such as item IDs etc

Based on this, I have the following SQL schema for my statistics table

CREATE TABLE IF NOT EXISTS `statistics` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`account_id` int(11) NOT NULL,
	`project_id` int(11) NOT NULL,
	`user_id` int(11) NOT NULL,
	`section` varchar(32) NOT NULL,
	`action` varchar(32) NOT NULL,
	`when` int(11) NOT NULL,
	`uri` varchar(255) NOT NULL,
	PRIMARY KEY (`id`)
)

And lastly, the actual code for the hook which is pretty self explanatory

<?php if(!defined('BASEPATH')) exit('No direct script access allowed');
class Statistics {
	public function log_activity() {
		// We need an instance of CI as we will be using some CI classes
		$CI =& get_instance();

		// Start off with the session stuff we know
		$data = array();
		$data['account_id'] = $CI->session->userdata('account_id');
		$data['project_id'] = $CI->session->userdata('project_id');
		$data['user_id'] = $CI->session->userdata('user_id');

		// Next up, we want to know what page we're on, use the router class
		$data['section'] = $CI->router->class;
		$data['action'] = $CI->router->method;

		// Lastly, we need to know when this is happening
		$data['when'] = time();

		// We don't need it, but we'll log the URI just in case
		$data['uri'] = uri_string();

		// And write it to the database
		$CI->db->insert('statistics', $data);
	}
}

And that’s it! I hope this was of use to you, let me know in the comments if you have any possible improvements to this (There must be plenty, but this is just an example of something basic). And even better, share how you’re using this in your applications, how are you querying the data to help you?

16 Feb 2010

Easy chained select using jQuery

jQuery 2 Comments

Whilst working on Project Trackr, I had the need to be able to easily add chained selects within a form, more specifically, I wanted selecting an account or a client from a drop down list to load another drop down list beneath it with users in that account. An easy way to do this would be to register a listener on a given ID, when something in that dropdown is selected, fire an AJAX event and then load the response contents into a pre specified div. Easy enough and it works, but I wasn’t satisfied.

I wanted a solution which was a simple as adding a class to the first drop down, that’s it. A couple of minutes later, I had a working solution, and I find it to be very useful, so thought I would share it with you.

The solution is made up of 3 parts, the HTML, JavaScript and the HTML, which I will now go through one at a time.

The HTML

There is absolutely nothing fancy here, at all, so much so, that I’ve decided to not bother with the rest of the form/HTML page and just show the relevant field.

<p>
	<label for="form_client">Client</label>
	<?php echo form_error('client_id')?>
	<?php echo HTML::client_dropdown($clients, false, array('class'=>'chain_user'))?>
</p>

A couple of quick points here, I wrap each form field, label and error (not shown here) in a set of paragraph tags, which allows me to style them using CSS. The client_dropdown function returns a select element containing the clients passed in. This results in something like

<select class="chain_user" id="form_client">
	<option></option>
	<option value="1">Account 1</option>
	<option value="2">Account 2</option>
	<option value="3">Account 3</option>
	<option value="4">Account 4</option>
	<option value="5">Account 5</option>
	<option value="6">Account 6</option>
</select>

The Javascript

I’m a huge fan of jQuery, so obviously that’s what I’m using here. The code itself is well commented, so it should be pretty self explanatory

var SITE_URL	= 'http://localhost/project/';
var LOADING		= '<img src="'+SITE_URL+'images/loading.gif" class="loading" />';

$(document).ready(function() {
	/**
	 * Listen out for chain events
	 * In this case, any select which has a class "chain_user"
	 * It will load a dropdown with all the users in that account beneath it
	 */
	$('select.chain_user').change(function() {
		// First, we need the account ID
		// Leave now if this is not set (They selected the empty item at the top)
		var account_id = $(this).val();
		if(account_id == '') return false;

		// Initialise any objects we need now or later
		// Create these now as $(this) will not be the real this in the response
		var parent = $(this).parent();
		var next = parent.next();

		// We need to put a loading gif in place of where the users drop down will be
		// We need to make sure we don't already have an AJAX loaded dropdown
		// otherwise we could end up with several - Disaster!
		if(next.hasClass('loaded-via-ajax')) {
			next.html(LOADING+' Loading users');
		} else {
			var p = new jQuery('<p class="loaded-via-ajax">'+LOADING+' Loading users</p>');
			parent.after(p);
			// We need to update what "next" is so that it works within the AJAX response
			next = parent.next();
		}

		// Now use AJAX to fetch a dropdown with all the users
		$.ajax({
			type: 'GET',
			url: SITE_URL+'account/ajax/get_users_dropdown/'+account_id,
			success: function(response) {
				next.html(response);
			}
		});
	});
});

The PHP

I’ve used PHP here, but you could use any server side language you want here. The code which gets called by the AJAX function is

$account = new Account_model($account_id);
$users = $account->get_users();

// We also need spit out a label
echo '<label for="form_user">User</label>';
echo HTML::user_dropdown($users);

The user_dropdown function is similar to the client_dropdown function I used to generate the first dropdown and is shown below

public static function user_dropdown($users, $selected = false, $attributes = array()) {
	// Merge the incoming attributes with the defaults
	$defaults = array(
		'name'=>'user_id',
		'title'=>'Select the user from this list',
		'id'=>'form_user'
	);
	$attributes = array_merge($defaults, $attributes);

	// Take care of any attributes we have
	$tmp = '';
	foreach($attributes as $key=>$value) {
		$tmp.= ' '.$key.'="'.$value.'"';
	}

	// Build up the select item
	$str = '<select'.$tmp.'>';
	foreach($users as $user) {
		$str.= '<option value="'.$user->id.'"'.($selected == $user->id ? ' selected="selected"' : '').'>'.$user->first_name.' '.$user->last_name.'</option>';
	}
	$str.= '</select>';
	return $str;
}

And that’s all there is to it! I hope you find this useful

20 Jan 2010

Getting full auto complete with CodeIgniter in Eclipse

CodeIgniter 31 Comments

CodeIgniter is a brilliant PHP framework and Eclipse is a brilliant IDE, so if I use them both, then it will be super brilliant? Not quite. In CodeIgniter, if you want to use one of the many fabulous libraries, such as the Database Active Record class, then you need to load it (using autoload or manually) but that happens at run time. What this means is Eclipse (and most IDEs for that matter) don’t know what $this->db is which means you don’t get auto complete.

This isn’t really a problem, but it’s a great place to have a couple of key strokes here and there, and we programmers are all about laziness efficiency. In order to enable auto completion, you need to follow these simple steps (I assume you already have Eclipse installed and a CodeIgniter project set up. I will also assume the default directory structure… if you know how to change it around I assume you’ll know where to find the relevant files) Read more