Extending DataMapper for CodeIgniter

Sarfraz Ahmed    April 19, 2015 11:35 AM

For one of my projects I was using CodeIgniter framework with goodies like DataMapper (by wanwizard.eu), HMVC Module, View Objects and more. Everything was smooth and sound until client requested that some of database tables must be encrypted. I was already encrypting sensitive information like passwords, etc but client wanted encryption for more tables such that:

  • One-way hashing, once data is encrypted, it can't be decrypted, we chose AES (Advanced Encryption Standard)
  • For same piece of text, hashing should be able to generate different encrypted text always so no two same text should sound like similar
  • Code should know whether provided piece of text is already encrypted or not

I created the encryption class and did some trickery so that it always gave different encryption text for the same string. And also used some kind of prefix information that would later let the code know whether or not something is already encrypted.

The real problem for me was that by the time project had turned into huge codebase, it would have taken ages to go and modify code everywhere so that new requirement of encryption is applied everywhere. I was using DataMapper library everywhere to interact with database and model classes looked like this:

class Api_client extends DataMapper
{
    # object properties
    public $id;
    public $appid;
    public $apikey;
    public $request_uri;

    # relations
    public $has_one = array('client');

    # validation
    public $validation = array(
        'appid' => array(
            'label' => 'App ID',
            'rules' => array('required', 'trim')
        ),
        'apikey' => array(
            'label' => 'API Key',
            'rules' => array('required', 'trim')
        ),
        'request_uri' => array(
            'label' => 'Request URI',
            'rules' => array('required', 'trim')
        )
    );

    # Default to ordering by id
    public $default_order_by = array('id' => 'desc');

    # Optional - useful if a record is to be retrieved by ID eg $user = new User(1);
    public function __construct($id = null)
    {
        parent::__construct($id);
    }

    # Optional - post model initialisation code
    public function post_model_init($from_cache = false)
    {
    }
}

We can see that each model class extends the data mapper:

class Api_client extends DataMapper

This is what was a clue to me. So in order to avoid modifying lots of code in whole codebase, I knew I can only extend this data mapper and inject my functionality the way I needed.

I actually needed the ability to:

  • automatically encrypt given fields in some table when saving them to database
  • automatically getting the right value when reading back from database

By that I mean, instead of going everywhere in codebase and modifying code to encrypt certain fields like this:

$apiClient = new Api_client();
$apiClient->appid = Encode($appId);     // encrypt this field value
$apiClient->apikey = Encode($apikey);  // encrypt this field value
$apiClient->request_uri = $request_uri;
$apiClient->save(); // save to db

I simply wanted to leave current code as is without modifying it:

$apiClient = new Api_client();
$apiClient->appid = $appId;
$apiClient->apikey = $apikey;
$apiClient->request_uri = $request_uri;
$apiClient->save(); // save to db

In this case, I wanted data mapper to automatically encrypt the appid and apikey values for me. Now imagine I had this code placed in quite some files, it would have been time-consuming process to modify and add Encode() function calls manually everywhere.

In order to do that, I simply told data mapper which fields need to be encrypted:

class Api_client extends DataMapper
{
    ////////////////////

    // for encryption fields
    private $encryptFields = array(
       'appid',
       'apikey',
    );    
}

Now if you look at the code of data mapper, you would see it uses _to_object() function to map fields and save() function to save the info to database. So I tapped into these in my child classes and modified them a bit so that $encryptFields are auto-magically encrypted on my behalf. Since we are extending data mapper (class Api_client extends DataMapper), I modified it like this to do the encryption for me:

class Api_client extends DataMapper
{
    # object properties
    public $id;
    public $appid;
    public $apikey;
    public $request_uri;

    # relations
    public $has_one = array('client');

    # validation
    public $validation = array(
    'appid' => array('label' => 'App ID', 'rules' => array('required', 'trim')), 
    'apikey' => array('label' => 'API Key', 'rules' => array('required', 'trim')), 
    'request_uri' => array('label' => 'Request URI', 'rules' => array('required', 'trim'))
    );

    // for encryption fields
    private $encryptFields = array('appid', 'apikey');

    # Default to ordering by id
    public $default_order_by = array('id' => 'desc');

    # Optional - useful if a record is to be retrieved by ID eg $user = new User(1);
    public function __construct($id = null)
    {
        parent::__construct($id);
    }

    # Optional - post model initialisation code
    public function post_model_init($from_cache = false)
    {
    }

    // extending date modal here //
    public function _to_object($item, $row)
    {
        // Populate this object with values from first record
        foreach ($row as $key => $value) {
            if ($this->isEncryptedField($key)) {
                $item->{$key} = decodeField($value);
            } else {
                $item->{$key} = $value;
            }
        }

        foreach ($this->fields as $field) {
            if (!isset($row->{$field})) {
                $item->{$field} = null;
            }
        }

        // Force IDs to integers
        foreach ($this->_field_tracking['intval'] as $field) {
            if (isset($item->{$field})) {
                $item->{$field} = intval($item->{$field});
            }
        }

        if (!empty($this->_field_tracking['get_rules'])) {
            $item->_run_get_rules();
        }

        $item->_refresh_stored_values();

        if ($this->_instantiations) {
            foreach ($this->_instantiations as $related_field => $field_map) {
                // convert fields to a 'row' object
                $row = new stdClass();
                foreach ($field_map as $item_field => $c_field) {
                    $row->{$c_field} = $item->{$item_field};
                }

                // get the related item
                $c =& $item->_get_without_auto_populating($related_field);
                // set the values
                $c->_to_object($c, $row);

                // also set up the ->all array
                $c->all    = array();
                $c->all[0] = $c->get_clone();
            }
        }
    }

    public function save($object = '', $related_field = '')
    {
        // Temporarily store the success/failure
        $result = array();

        // Validate this objects properties
        $this->validate($object, $related_field);

        // If validation passed
        if ($this->valid) {

            // Begin auto transaction
            $this->_auto_trans_begin();

            $trans_complete_label = array();

            // Get current timestamp
            $timestamp = $this->_get_generated_timestamp();

            // Check if object has a 'created' field, and it is not already set
            if (in_array($this->created_field, $this->fields) && empty($this->{$this->created_field})) {
                $this->{$this->created_field} = $timestamp;
            }

            // SmartSave: if there are objects being saved, and they are stored
            // as in-table foreign keys, we can save them at this step.
            if (!empty($object)) {
                if (!is_array($object)) {
                    $object = array(
                        $object
                    );
                }

                $this->_save_itfk($object, $related_field);
            }

            // Convert this object to array
            $data = $this->_to_array();
            $data = $this->changeWithEncrypted($data);
            //pretty_print($data);

            if (!empty($data)) {
                if (!$this->_force_save_as_new && !empty($data['id'])) {
                    // Prepare data to send only changed fields
                    foreach ($data as $field => $value) {
                        // Unset field from data if it hasn't been changed
                        if ($this->{$field} === $this->stored->{$field}) {
                            unset($data[$field]);
                        }
                    }

                    // if there are changes, check if we need to update the update timestamp
                    if (count($data) && in_array($this->updated_field, $this->fields) && !isset($data[$this->updated_field])) {
                        // update it now
                        $data[$this->updated_field] = $this->{$this->updated_field} = $timestamp;
                    }

                    // Only go ahead with save if there is still data
                    if (!empty($data)) {
                        // Update existing record
                        $this->db->where('id', $this->id);
                        $this->db->update($this->table, $data);

                        $trans_complete_label[] = 'update';
                    }

                    // Reset validated
                    $this->_validated = false;

                    $result[] = true;
                } else {
                    // Prepare data to send only populated fields
                    foreach ($data as $field => $value) {
                        // Unset field from data
                        if (!isset($value)) {
                            unset($data[$field]);
                        }
                    }

                    // Create new record
                    $this->db->insert($this->table, $data);

                    if (!$this->_force_save_as_new) {
                        // Assign new ID
                        $this->id = $this->db->insert_id();
                    }

                    $trans_complete_label[] = 'insert';

                    // Reset validated
                    $this->_validated = false;

                    $result[] = true;
                }
            }

            $this->_refresh_stored_values();

            // Check if a relationship is being saved
            if (!empty($object)) {
                // save recursively
                $this->_save_related_recursive($object, $related_field);

                $trans_complete_label[] = 'relationships';
            }

            if (!empty($trans_complete_label)) {
                $trans_complete_label = 'save (' . implode(', ', $trans_complete_label) . ')';
            } else {
                $trans_complete_label = '-nothing done-';
            }

            $this->_auto_trans_complete($trans_complete_label);

        }

        $this->_force_save_as_new = false;

        // If no failure was recorded, return TRUE
        return (!empty($result) && !in_array(false, $result));
    }

    private function isEncryptedField($key)
    {
        if (false !== in_array($key, $this->encryptFields)) {
            return true;
        }

        return false;
    }

    private function changeWithEncrypted(array $array)
    {
        foreach ($array as $key => $value) {
            if ($this->isEncryptedField($key)) {
                if ($value !== '') {
                    $array[$key] = encodeField($value);
                } else {
                    $array[$key] = $value;
                }
            }
        }

        return $array;
    }

    private function _get_generated_timestamp()
    {
        // Get current timestamp
        $timestamp = ($this->local_time) ? date($this->timestamp_format) : gmdate($this->timestamp_format);

        // Check if unix timestamp
        return ($this->unix_timestamp) ? strtotime($timestamp) : $timestamp;
    }

}

In above class, most of the code remains same for _to_object() and save() functions as in original data mapper class but I have modified few places so that I can put encryption for needed fields.

So in conclusion, we learned how we tapped onto the data mapper class and extended it for our needs of auto-encryption of told fields. If you happen to have similar requirement or you wanted to inject your own functionality to the data mapper, you can do that through the use of _to_object() and save() functions of data mapper.







Comments powered by Disqus