I've written about model scoping before, but I thought I'd revisit it with some lessons I've learnt since last time. The essence of model scoping is that you can chain named methods together that add certain parameters onto your query. In a similar way that CodeIgniter's ActiveRecord allows you to chain methods together to build queries, you can use model scoping to incrementally add onto your model query.
The trick to writing a scope is to return $this. This lets PHP chain the methods onto one-another, resulting in a nice syntax. Here's an example:
class User_model extends CI_Model {
public function __construct() {
parent::__construct();
}
public function confirmed() {
$this->db->where('confirmed', 1);
return $this;
}
public function sorted($column = 'date') {
$this->db->order_by($column);
return $this;
}
public function get() {
return $this->db->get('users')->result();
}
}
With these methods defined, we can produce some nice, readable code to pull the results from the database.
$this->user_model->confirmed()
->sorted()
->get();
Comparing this to what we'd write if we didn't define those lookups as scopes:
$this->db->where('confirmed', 1)
->order_by('date')
->get('users')
->result();
It's obvious that the former is much cleaner and more readable. We can add or remove as many as we like, and they can be as comprehensive or as simple as we like. It doesn't even have to be limited to database lookups. We're sharing an instance of the class, so we can add things to an instance variable which we can then print, or we can perform other data-related processes all scoped out by our method scopes.
While this is generally all rosy, you may come across something like this:
public function favourited() {
// Get the favourites
$fav = $this->db->select('id')->get('favourites')->result();
$ids = array();
foreach ($fav as $row) {
$ids[] = $row->id;
}
// Restrict the current query
$this->db->where_in('id', $ids);
return $this;
}
This will only work if it's called first. You can easily get conflicts in SQL queries when you're chaining and making separate queries inside the scopes. You can start to look for columns that don't exist, or affect the query in some other bad way.
The solution to this problem is to isolate the query. Take the current values from the class you're accessing (in our case the CodeIgniter ActiveRecord class), cache them, clear them, make the query, and then reset the variables to their original state. This makes sure the query starts with a blank slate.
public function favourited() {
// Cache the CodeIgniter ActiveRecord values
$ar_values = array(
'ar_select', 'ar_distinct', 'ar_from', 'ar_join', 'ar_where', 'ar_like',
'ar_groupby', 'ar_having', 'ar_limit', 'ar_offset', 'ar_order', 'ar_orderby',
'ar_set', 'ar_wherein', 'ar_aliased_tables', 'ar_store_array'
);
foreach ($ar_values as $val) { $$val = $this->db->$val; $this->db->$val = null; }
// Get the favourites
$fav = $this->db->select('id')->get('favourites')->result();
$ids = array();
foreach ($fav as $row) {
$ids[] = $row->id;
}
// Restore the AR values
foreach ($ar_values as $val) { $this->db->$val = $$val; }
// Restrict the current query
$this->db->where_in('id', $ids);
return $this;
}
Now we can use the favourited() scope without fear!
$this->user_model->confirmed()
->sorted()
->favourited()
->get();
As you can see, scopes are a powerful and flexible way of creating really, really nice semantic and clean code. I encourage you to start using them today.