Extending DataMapper for CodeIgniter
Sarfraz Ahmed April 19, 2015 11:35 AMFor 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.