Interfaces in Codeigniter

Interfaces in Codeigniter (sort of)

If you work at all with Codeigniter you're at least somewhat aware of two major themes that have been around for the last year or so: Laravel kicks ass, and Codeigniter is pushing up daisies

The internal debate many of us has been having is whether or not to continue with CI, or simply move on to greener pastures. Ellis Labs has certainly been doing no one any favors toward keeping CI alive. However, most of us still have a lot invested - and a lot of active codebases - in Codeigniter, and need to find ways to keep it relevant. I personally feel that CI is an open source set of code that we can do whatever we like with, and that many of these new ideas we are being presented with can be "ported back" in one form or another. So I'm beginning my discovery series of "Codeigniter can have Nice Things, too"

Now, let's not gild the sow's ear - CI can be a real bear to try to apply OOP designs to, and often we have to fake it to get the results we want. However, I don't think that's necessarily a bad thing. We don't need the actual implementation of the design, we need its benefits. This we can do.

So I'm going to go ahead and offend the purists and misuse all the terms, and show you an idea that I think will help make your code more robust and bug-free.

Let's layout the scenario: You have a script, probably in a cron job, that runs daily and cycles over all the "Teachers" in your system with a status of "Y" and does an action to them. Standard stuff, been running for ages, never had a problem. It looks like this:

$teachers = $this->teacher_model->get(array('status'=>'Y'));
foreach ($teachers as $teacher)
{
   // ...do good stuff
}

Until one day...we need to do this same action to "Advisers", but on different days. Okay, so we should have been passing the result set into this function anyway, so we'll refactor our code at the same time and just cycle over teachers or advisers, depending which we pass in. Aren't we clever!

Then one day the boss wants some additional work done on a different Teachers field, so we just add it in the script - and it breaks. Because Advisers don't have that field. So we have to go add a "dummy field" to our Advisers query, or add an isset(), or some similar hack.

Familiar?

Let's talk about Interfaces and Abstract classes.

I don't want to make this a discussion about design theory (especially as I'm going to fake it a bit in our code). Instead, I just want us to think about and apply the idea to our code to help it be more rugged. While there are specific definitions - both in theory and actual code restrictions - of interfaces and abstracts in php, what I'm after today is the idea of a Prototype - a contract such as an interface supplies, but that also ensures our member variables are present. In pure code, it would look something like:

class Foo extends Bar implements Zaz{
}

but we're going to stay simple today and only use an Abstract class. Of course, I encourage you to do further studying and apply the ideas more fully.

So, assuming we have a table Users with fields first_name and last_name, let's write some basic code for our cron script:

public function index()
{
	// of course you know to do this in a model...
	$users = $this->db->query('SELECT * FROM users')->result();

	foreach ($users as $user)
	{
		$this->process_users($user);
	}
}

function process_users($user)
{
    $user->full_name = $user->first_name . ' ' . $user->last_name; 

    // ... do something to $user->full_name
}

With this, we are simply passing in a Codeigniter object - a database row() - to our function. But we know this is bad already, for reasons we discussed above. If we extend this for Teachers to an additional field, and then try to use it for Advisers or other system users - fail. The problem comes from our using a row() object, which is dynamic and shaped by the query. We need to define something consistent. Let's add a library class:

class Teacher
{
	var $first_name;

	var $last_name;

	var $full_name;

	var $class_var_1 = 'Wow';

	function speak($what = 'Uhm...')
	{
		return $what;
	}
}

So that's great, we have a class with the fields we want. But how do we make use of it? Well, you may not have heard of this one, but you can just:

public function index()
{
    $this->load->library('teacher');

    // of course you know to do this in a model...
    $users = $this->db->query('SELECT * FROM users')->result('teacher');

    foreach ($users as $user)
    {
	$this->process_users($user);
    }
}

...

Did you see where I passed "teacher" into the result() function? Codeigniter can actually return a result object, result array...or a class of your choice. Let's extend our code a little bit to see how really cool this can be for us.

public function index()
{
	$this->load->library('Teacher');		

	$users = $this->db->query('SELECT * FROM users')->result('teacher');

	foreach ($users as $user)
	{
		$this->process_users($user);
	}

}

function process_users($user)
{
   // call attributes
    $user->full_name = $user->first_name . ' ' .$user->last_name; 

   echo $user->full_name;

   //call a function
   echo $user->speak('Hello, ' . $user->first_name);

   //call a class variable not in our query
   echo $user->class_var_1;
}

Nice, isn't it? CI automatically assigns all the row variables to the class, but also makes the entire class available. Now we're passing in defined objects. But this doesn't help us with a couple of issues - how do we make sure that when we pass in an Adviser, it doesn't explode; and how can we do slightly different actions on each user without a spaghetti mess of conditional checks?

Here's where we'll bring in Interfaces (or at least the idea of them) and Abstract classes. There are some different ways to set this up, as I mentioned, but let's just get the job done. We want to define certain functions and certain variables that our script is going to be looking for. We'll make an Abstract class because php interface objects don't allow for member variables. So make a new directory at the same level as your libraries and call it "interfaces" (or "abstracts", if you must). Add this file, and change our Teacher.php file:

// file application/interfaces/user.php

abstract class User 
{
	var $first_name;

	var $last_name;

	var $full_name;

	var $class_var_1 = 'Wow';

	public function speak(){}
}

// file application/libraries/teacher.php

//you could autoload instead, if you want to bootstrap
include APPPATH.'interfaces/user.php';  

class Teacher extends User
{
	function speak($what = 'Uhm...')
	{
		return $what;
	}
}

// and one final change... in your cron script:

function process_users(User $user)

What have we done? The key is actually that last line, where we force the process_user() function to only accept User objects. By doing this, we can ensure the integrity of our script. As long as the top-most object meets the needs of our script, all its inherited objects will too. We prevent ourselves from lazily passing in some new class that doesn't fit our requirements (but might only fail under certain conditions).

Not only that, but if we want speak() to behave differently on the Adviser class, it doesn't matter - the script will simply execute it with no need to use conditional statements to check what class type you are working on (although a BIG WARNING on that - it really isn't a good idea. Later, you'll forget exactly what's going on and might assume speak() is behaving the same way on everything, and then waste a day tracking some weird bug because of it. Look into some more advanced design patterns to see how to better handle this scenario. At the very least, use carefully).

Lastly, we started down the road of Testable Code by forcing ourselves to write a function which we can now pass in a mock object. All good stuff!

This is not the perfect rose-smelling code we'd like to have some day, but it is a start - much better than when we were just dumping Codeigniter result objects into the loop and praying. There's a lot more to learn and think about wih these ideas, but hopefully if you are new to them, it's stirred in you a hunger to learn more.

Codeigniter doesn't have to be a shackle around your ankles.

Hope that helped!

Contact me