Painless active record in codeigniter 2 (CI2)

After many years I left PHP, Today I have to maintain an application written in CI2. I must say I hate lots of thing in term of what the framework has to offer.

I start reading the CI2 developer docs for a few hours, I don’t really like the CI2 way of doing thing. CI2 is an old framework with lots of many crappy things, I ask myself if I can not use a better framework why don’t I improve the existing to make changes easier next time?

I sat down and started with the model layer by simplifying the active record then controller, view and validation.

I am not trying to change the whole framework, I balance the change vs output(deadline, clean code, easy to maintain).

As I am quite new to CI2 so I don’t expect to be perfect I might not have used all the tools that CI2 has already offered please leave your comments if you think it is a duplicate work.

Active Record

I am happy to see the CI2 active record implementation but when I take a look at it, what I see from the CI 2 active record lib is a kind of query builder instead.

Below is an example of expected active record model:

CREATE TABLE `user` (
`id` int(11) NOT NULL,
`first_name` varchar(255) NOT NULL,
`last_name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`role` int(11) NOT NULL,
 `sex` char(1) NOT NULL,
`dob` datetime NOT NULL,
`display_name` int(11) NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

And active record should look like this

//Query return object of this case
//You can encapsulate your logics inside this class easier than the // one returned from PHP sql driver.
class User extends ActiveRecordBase {
const ROLE_FREE = "free";
const ROLE_PREMIUM = "premium";
function full_name() {
$this->first_name + “ “ + $this->last_name
}
}

Read record

$user = User::find($id);
if($user)
$user->full_name();
$users = User::all(array("sex"=> "M"));
print_r($users);

Create a new record

$user = new User();
$filter_params = array(“first_name” => $_POST[“first_name”],
“last_name” => $_POST[“last_name”],
“dob => $_POST[“dob”],
“email” => $_POST[“email”],
“sex” => $_POST[“sext”],
“role” => User::ROLE_FREE);
$user->set_attributes($filter_params);
$user->save(); //true or false

Update record

$user = User::find($id);
//first approach set attrs then call save()
$user->set_attributes($filter_params);
$user->save();
//second approach update attrs directly
$user->update_attributes($filter_params);

Delete record

$user = User::find($id);
$user->delete();

Active record Implementation

Below is a simple Active record implement on top of a query builder(CI2 active record)

<?php
//application/models/active_record_base.php
class ActiveRecordBase {
const PER_PAGE = 20;
  //store validation error
protected $_errors = array();
  //track record changes when doing update
protected $_changes = array();
  //you field not part of table can not find a better way in php 5.2
protected $_not_sql_fields = array('_errors', '_changes', '_not_sql_fields');
// define your table primary key name here default is id
static function primary_key() {
return 'id';
}
// you must redefine the name of your table
static function table_name() {
throw new Exception("You must overide this method to return your database table name");
}
//you must redefine your class name php5.2
static function class_name() {
throw new Exception("You must overide this method to return your model name");
}
//if you have fields in model that are not part of db table field list them down here
static function accessible_fields(){
return array();
}
//I use form validation in model instead of in controller
function __construct() {
$this->load->library('form_validation');
parent::__construct();
}
//old code don't really know what it does yet
function load_other_model($model_name) {
$CI =& get_instance();
$CI->load->model($model_name);
return $CI->$model_name;
}
//validation errors
function get_errors() {
return $this->_errors;
}
//active record changes after touching object
function get_changes() {
return $this->_changes();
}
//where AR object is new or edit 
function new_record() {
return !$this->id();
}
//return the primary key value of AR object
function id() {
$primary_key = static::primary_key();
return $this->{$primary_key};
}
//a wrapper to return current time
function current_time() {
return date("Y-m-d H:i:s");
}
//disable timestampable by default(created_at, updated_at fields)
static function timestampable() {
return false;
}
//exclud fields that are not part of db table fields
function exclude_field($field_name){
$fields = array_merge($this->_not_sql_fields, static::accessible_fields());
return in_array($field_name, $fields);
}

function set_attribute($field, $value){
if($this->exclude_field($field) || $field == static::primary_key())
return;
$this->_changes[$field] = array($this->$field, $value);
$this->$field = $value;
}
function set_attributes($datas) {
$this->_changes = array();
foreach($datas as $field => $value)
$this->set_attribute($field, $value);
}
function get_attributes(){
$attributes = array();
foreach ($this as $field => $value) {
if($this->exclude_field($field) || $field == static::primary_key() )
continue;
$attributes[$field] = $value;
}
return $attributes;
}
//copy php sql driver object to active record object
function copy_object($record) {
foreach($record as $field => $value) {
if(!$this->exclude_field($field)){
$this->$field = $value;
}
}
return $this;
}
static function all($conditions, $page=1, $order_by=null ){
$limit = ActiveRecordBase::PER_PAGE;
$offset = $page < 2 ? 0 : ($page-1) * $limit;
  $class_name = static::class_name();
$active_record = new $class_name;
  $active_record->db->where($conditions);
$active_record->db->from(static::table_name());
$active_record->db->limit($limit);
$active_record->db->offset($offset);
  if($order_by)
$active_record->db->order_by($order_by);
  $query = $active_record->db->get();
$records = array();
  foreach( $query->result() as $record){
$active_record = new $class_name;
$active_record->copy_object($record);
$records[] = $active_record;
}
return $records;
}
static function find($id){
$class_name = static::class_name();
$active_record = new $class_name;
  $primary_key = static::primary_key();
$conditions = array($primary_key => $id);
$query = $active_record->db->get_where(static::table_name(), $conditions);
  if($query->num_rows() == 0)
return null;
else {
$result = $query->result();
$record = $result[0];
$active_record->copy_object($record);
return $active_record;
}
}
function insert(){
if(static::timestampable()) {
$this->created_at = $this->current_time();
$this->updated_at = $this->current_time();
}
  $this->db->insert(static::table_name(), $this->get_attributes());
  if($this->db->affected_rows() == 0)
return false;
else{
$primary_key = static::primary_key();
$this->{$primary_key} = $this->db->insert_id();
return true;
}
}
function update() {
$class_name = static::class_name();
if(static::timestampable())
$this->set_attribute('updated_at', $this->current_time());
  $this->db->where('id', $this->id());
$this->db->update(static::table_name(), $this->get_attributes());
  if($this->db->affected_rows() == 0)
return false;
else
return $this;
}
function delete(){
$this->db->delete(static::table_name(), array('id' => $this->id()));
if($this->db->affected_rows() == 0)
return false;
else
return true;
}
function update_attributes($datas){
$this->set_attributes($datas);
$class_name = static::class_name();
  if(static::timestampable())
$this->set_attribute('updated_at', $this->current_time());
  if(!$this->validate())
return false;
  $this->db->where('id', $this->id());
$this->db->update(static::table_name(), $this->get_attributes());
  if($this->db->affected_rows() == 0)
return null;
else
return $this;
}
function validate(){
$this->validation_rules();
if($this->form_validation->run() == false){
$this->_errors = $this->form_validation->errors();
return false;
}
return true;
}
function save($validate = true){
if($validate && !$this->validate())
return false;
return $this->new_record() ? $this->insert() : $this->update();
}
}

Define an active record model

<?php
//application/models/user.php
//table field name must match with the properties name
class User extends ActiveRecordBase {
var $id = null;
var $first_name = '';
var $last_name = '';
var $dob = null;
var $role = "";
var $email = "";
var $display_name = "";
  //we will user timestamp-able feature
var $created_at = null;
var $updated_at = null;
 //we tell that we want updated_at and created_at be set automatically by the AR
static function timestampable() {
return true; //default is false
}
//if your primary key is not id put here otherwise omit this method
static function primary_key() {
return "id";
}
//this model will link to database table called 'user'
static function table_name() {
return "user";
}
//can not find a better way to get current class name in php5.2
static function class_name(){
return 'User';
}
}

In the next post I will show how to user CI2 controller/action view painlessly.

ps: this post is highly inspired by rubyonrails framework mvc.