Mimicking Ruby on Rails model with CodeIgniter

One thing I like from Ruby on Rails is it’s model class, it makes it very simple to do your database things without have to write a bunch of SQL query (on general case). Following it’s famous ‘Convention over configuration’, you don’t even have to tell the model class what’s your table name or it’s columns, everything will be ‘magically’ run as you expected. An simple example of Ruby on Rails model class is like:

class Car < ActiveRecord::Base
end

With just this two line of code you’ll get a fully functional model class to work with. It’ll <em>magically</em> knows what’s the table name along with all the columns. You’ll be able to do things like:

jazz = Car.new
jazz.manufacturer = 'Honda'
jazz.save

or

honda_cars = Car.find_by_manufacturer('Honda')
honda_cars.each do |car|
  puts car.type
end

all without even declaring anything in the model class.
Now I want the same functionality of the model class with PHP, especially with CodeIgniter framework. Out of the box, CodeIgniter’s model class doesn’t know anything about your database table even if you name the model class with the database table’s name itself. So I start to fiddling around with php’s magic methods. I want something simple enough for me to use, at least it have to be able to access its related table columns without I have to explicitly told it to. Here I just extending the CodeIgniter’s model class and add some functionality in it.

<?php
class Emodel extends Model {

  var $primary_key = 'id';
  var $table_name;
  var $errors = array();
  var $active_db;

  // the constructor
  function Emodel($tbl_name = null,$attributes = null) {
    parent::Model(); // the parent class constructor
    $this->load->database();
    $this->appdb = $this->db;
    if (!is_null($tbl_name)) {
      $this->table_name = $tbl_name; // set the table name if it defined
    }
    else {
      // if not defined the table name is taken from the class name and lowercased
      $this->table_name = strtolower(__CLASS__);
    }
     // get all the information about this table's collumns
     // and set it as class variables
    $fields = $this->appdb->list_fields($this->table_name);
    foreach($fields as $field) {
      $this->_set_field(strtolower($field),null);
    }
    if (!is_null($attributes)) {
      $this->set_attributes($attributes);
    }
  }

  // the magic method
  // with this function we can get the columns value without have to declare it as a function
  // if called without argument, it fetch the column value eg. $this->name() = get the name
  // if called with argument, it will set the column value with the supplied argument,
  // eg. $this->name('anonymous') = set the name to anonymous
  function __call($method,$args) {
    if (count($args) > 0) {
      $this->_set_field(strtolower($method),$args[0]);
    }
    return $this->_get_field($method);
  }

  // this function will fetch all the column name and value into an array
  function fields() {
    $arr = array();
    $fields = $this->appdb->list_fields($this->table_name);
    foreach($fields as $field) {
      $arr[$field] = $this->_get_field($field);
    }
    return $arr;
  }

  // this function will get the particular column value, intended for in-class use aka private function
  function _get_field($field) {
    return eval('return $this->'.$field.';');
  }

  // this function will set the particular column value, intended for in-class use aka private function
  function _set_field($field,$value) {
    eval('$this->'.$field.' = $value;');
  }

  // this function will populate the array for keeping the list of required columns that cannot be blank
  // the array will be used when the user saving the record to database
  function validates_presence_of() {
    $this->presence_required = array();
    $args = func_get_args();
    foreach ($args as $arg)
    {
      $this->presence_required[] = $arg;
    }
  }

  // this function will populate the array for keeping the list of columns that the value cannot be same
  // the array will be used when the user saving the record to database
  function validates_uniqueness_of() {
    $this->uniqueness_required = array();
    $args = func_get_args();
    foreach ($args as $arg)
    {
      $this->uniqueness_required[] = $arg;
    }
  }

  // this function will populate the array for keeping the list of columns that the value must be numbers
  // the array will be used when the user saving the record to database
  function validates_numericality_of() {
    $this->numericality_required = array();
    $args = func_get_args();
    foreach ($args as $arg)
    {
      $this->numericality_required[] = $arg;
    }
  }

  function _validate() {
    return $this->run_validation();
  }

  // this function should be override in the child class to provide custom validation
  function validate() {
    // override this in child class to create custom validation
  }

  // this function will run the actual validation
  // first it will chekced the column that cannot be blank
  // and then it will checked for columns that must have unique value
  // other validation can be added as well
  function run_validation() {
    if (count($this->presence_required) > 0) {
      foreach ($this->presence_required as $field)
      {
        $s = explode(' ',$field);
        $f = $s[0];
        if (trim($this->_get_field($f)) == '') {
          $this->errors[$field] = "tidak boleh kosong";
        }
      }
    }
    if (count($this->uniqueness_required) > 0) {
      foreach ($this->uniqueness_required as $field)
      {
        $s = explode(' ',$field);
        $f = $s[0];
        $this->appdb->select($f);
        $this->appdb->where($f, $this->_get_field($f));
        $recs = $this->get_record();
        if (!is_null($recs)) {
          $this->errors[$field] = "sudah dipakai";
        }
      }
    }
    if (count($this->numericality_required) > 0) {
      foreach ($this->numericality_required as $field)
      {
        $s = explode(' ',$field);
        $f = $s[0];
        if (!is_numeric(trim($this->_get_field($f)))) {
          $this->errors[$field] = "harus berupa angka";
        }
      }
    }
    $this->validate(); // run the custom validation
    return count($this->errors) > 0 ? FALSE : TRUE;
  }

  // this function will show the errors when the user try to save the record
  // TODO: this function should be more general
  function show_errors() {
    $html = '';
    if (count($this->errors) > 0) {
      $html = "<div id='error_explanation'>";
      $html .= "<h3>Terdapat beberapa kesalahan :</h3><ul>";
      foreach ($this->errors as $error => $desc)
      {
        $html .= '<li>';
        $s = explode(' ',$error,3);
        if (isset($s[1]) && $s[1] == 'as') {
          $html .= ucfirst(str_replace('_',' ',$s[2])).' '.$desc;
        }
        else {
          $html .= ucfirst(str_replace('_',' ',$error)).' '.$desc;
        }
        $html .= '</li>';
      }
      $html .= '</ul></div>';
    }
    return $html;
  }

  function get_records() {
    $query = $this->appdb->get($this->table_name);
    if ($query->num_rows() > 0) {
      return $query->result_array();
    }
    else {
      return null;
    }
  }

  function get_record() {
    $query = $this->appdb->get($this->table_name);
    if ($query->num_rows() > 0) {
      return $query->row_array();
    }
    else {
      return null;
    }
  }

  function find($key) {
    $this->appdb->where($this->primary_key,$key);
    return $this->get_record();
  }

  function set_attributes($attributes) {
    foreach ($attributes as $key => $value) {
      eval('$this->'.$key.' = "'.$value.'";');
    }
  }

  function all() {
    return $this->get_records();
  }

  // this function will determine whether we're going to create new record or just update the record
  // it will do the actual save/update if the validation were passed
  // upon success it will return the saved record
  // TODO: should return the updated record when success in updating
  function save() {
    if ($this->_validate()) {
      foreach ($this->fields() as $key => $value) {
        $data[$key] = $value;
      }
      $this->appdb->where($this->primary_key, $this->_get_field($this->primary_key));
      $res = $this->appdb->get($this->table_name);
      if ($res->num_rows() == 0) {
        $this->appdb->insert($this->table_name,$data);
        $this->id($this->appdb->insert_id());
      }
      else {
        $this->appdb->where($this->primary_key, $this->_get_field($this->primary_key));
        $this->appdb->update($this->table_name,$data);
      }
      return $this;
    }
    else {
      return false;
    }
  }

  // this function is only needed when you have to connect to another database when the class already instantiated
  // the app_db_config_for function is just returning the database configuration, or you can set it up manually
  function set_database($database) {
    $this->active_db = $database;
    $config = app_db_config_for($database);
    $CI =& get_instance();
    $this->appdb = $CI->load->database($config,true);
  }
}
?>

with the above class, now I can do:

<?php

// this would expect a table named 'user' in the database
class User extends Emodel {
  function User() {
    parent::Emodel();
    $this->validates_presence_of('login','password','email','fullname');
    $this->validates_uniqueness_of('login','email');
  }
}

$user = new User();
$user->login('user1');
$user->password('secret');
$user->email('user1@example.com');
$user->fullname('User number 1');
$user->save();
?>

pretty much like Ruby on Rails huh? :)
next thing to do is to add the relationship mechanism (which I still don’t know how to implements right now) but I think I’m going to use this for now :).

Advertisements

One thought on “Mimicking Ruby on Rails model with CodeIgniter

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s